From 809dc264af14b1440c6505c60da0628d90a37cdd Mon Sep 17 00:00:00 2001 From: airano Date: Wed, 18 Feb 2026 18:55:12 +0330 Subject: [PATCH] =?UTF-8?q?fix(qa):=20resolve=20remaining=20v3.0.1=20issue?= =?UTF-8?q?s=20=E2=80=94=20CI,=20code=20bugs,=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI: Fix black formatting in core/site_registry.py Code: list_projects uses SiteManager, WP Advanced health uses REST API Docs: Fix transport types, endpoint guide, env var reference for all plugins Co-Authored-By: Claude Opus 4.6 --- DOCKER_README.md | 43 +++++- core/project_manager.py | 4 +- core/site_registry.py | 4 +- docs/getting-started.md | 222 +++++++++++++++++++-------- plugins/wordpress_advanced/plugin.py | 20 +-- server.py | 60 +++++--- 6 files changed, 245 insertions(+), 108 deletions(-) diff --git a/DOCKER_README.md b/DOCKER_README.md index 92228ec..e36a1dc 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -9,7 +9,7 @@ ### 1. Create a `.env` file ```bash -# Required +# Recommended (auto-generates temp key if omitted — check container logs) MASTER_API_KEY=your-secure-key-here # Add at least one WordPress site @@ -41,13 +41,14 @@ curl http://localhost:8000/health ### 4. Connect your AI client -In Claude Desktop's `claude_desktop_config.json`: +**Claude Desktop** (`claude_desktop_config.json`): ```json { "mcpServers": { "mcphub": { - "url": "http://localhost:8000/mcp", + "type": "streamableHttp", + "url": "http://localhost:8000/wordpress/mcp", "headers": { "Authorization": "Bearer your-secure-key-here" } @@ -56,15 +57,45 @@ In Claude Desktop's `claude_desktop_config.json`: } ``` +**VS Code** (`.vscode/mcp.json`): + +```json +{ + "servers": { + "mcphub": { + "type": "http", + "url": "http://localhost:8000/wordpress/mcp", + "headers": { + "Authorization": "Bearer your-secure-key-here" + } + } + } +} +``` + +> Use a plugin-specific endpoint (e.g., `/wordpress/mcp`) instead of `/mcp` to reduce tool count and save tokens. See [Endpoints](#endpoints) below. + ## Authentication -MCP Hub uses **Bearer token** authentication. Include the `Authorization` header in all requests: +MCP Hub uses **Bearer token** authentication: ``` -Authorization: Bearer YOUR_MASTER_API_KEY +Authorization: Bearer YOUR_API_KEY ``` -This applies to both MCP clients and API calls. Query parameter auth is not supported. +> `X-API-Key` header and query parameter auth are **not** supported. + +## Endpoints + +Use the most specific endpoint for your use case: + +| Endpoint | Tools | `site` param? | Best for | +|----------|------:|:-------------:|----------| +| `/project/{alias}/mcp` | 22-100 | No (pre-scoped) | Single-site workflow | +| `/{plugin}/mcp` | 23-101 | Yes | Multi-site management | +| `/mcp` | 596 | Yes | Admin & discovery only | + +Available plugin endpoints: `/wordpress/mcp`, `/woocommerce/mcp`, `/wordpress-advanced/mcp`, `/gitea/mcp`, `/n8n/mcp`, `/supabase/mcp`, `/openpanel/mcp`, `/appwrite/mcp`, `/directus/mcp`, `/system/mcp` ## Using Docker Compose diff --git a/core/project_manager.py b/core/project_manager.py index b41cbc6..c7c39de 100644 --- a/core/project_manager.py +++ b/core/project_manager.py @@ -91,9 +91,7 @@ class ProjectManager: if config: self._create_project_instance(plugin_type, project_id, config) except Exception as e: - self.logger.error( - f"Failed to create {plugin_type} project '{project_id}': {e}", exc_info=True - ) + self.logger.debug(f"Legacy ProjectManager: skipped {plugin_type}/{project_id}: {e}") def _load_project_config(self, plugin_type: str, project_id: str) -> dict[str, Any] | None: """ diff --git a/core/site_registry.py b/core/site_registry.py index 0acaa67..262eefa 100644 --- a/core/site_registry.py +++ b/core/site_registry.py @@ -98,9 +98,7 @@ class SiteRegistry: 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.info( - f" Alias '{alias}': {winner} (winner), {losers} (using full_id)" - ) + self.logger.info(f" Alias '{alias}': {winner} (winner), {losers} (using full_id)") # Reserved words that should NOT be interpreted as site IDs RESERVED_SITE_WORDS = { diff --git a/docs/getting-started.md b/docs/getting-started.md index 6f72371..ba32d1d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -173,49 +173,123 @@ RATE_LIMIT_PER_DAY=10000 ### WordPress Plugin Requirements -Some WordPress tools require additional plugins on your WordPress site: +Some tools require specific WordPress plugins or infrastructure: -| 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) | +| Tools | Requirement | +|-------|-------------| +| `wordpress_get_post_seo`, `wordpress_update_post_seo`, `wordpress_get_product_seo`, `wordpress_update_product_seo` | **Yoast SEO** or **RankMath** | +| `wordpress_wp_cache_*`, `wordpress_wp_db_*`, `wordpress_wp_plugin_*`, `wordpress_wp_theme_*`, `wordpress_wp_core_*`, `wordpress_wp_search_replace_dry_run` (15 tools) | Docker socket + `CONTAINER` env var | +| `wordpress_advanced_*` database/system tools | Docker socket + `CONTAINER` env var | +| `woocommerce_*` | **WooCommerce** plugin (separate `WOOCOMMERCE_` config) | -### Docker Socket for WP-CLI Tools +### Docker Socket for WP-CLI / WordPress Advanced -WP-CLI tools (cache management, database export, plugin updates via CLI) require Docker socket access: +WP-CLI and WordPress Advanced system tools require Docker socket access: -1. Add the container name to your `.env`: - ```bash - WORDPRESS_SITE1_CONTAINER=your-wp-container-name - ``` +```yaml +services: + mcphub: + image: airano/mcphub:latest + ports: + - "8000:8000" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + WORDPRESS_SITE1_URL: https://your-site.com + WORDPRESS_SITE1_USERNAME: admin + WORDPRESS_SITE1_APP_PASSWORD: xxxx xxxx xxxx xxxx + WORDPRESS_SITE1_CONTAINER: wordpress-container-name + WORDPRESS_SITE1_ALIAS: mysite + WORDPRESS_ADVANCED_SITE1_URL: https://your-site.com + WORDPRESS_ADVANCED_SITE1_USERNAME: admin + WORDPRESS_ADVANCED_SITE1_APP_PASSWORD: xxxx xxxx xxxx xxxx + WORDPRESS_ADVANCED_SITE1_CONTAINER: wordpress-container-name + WORDPRESS_ADVANCED_SITE1_ALIAS: mysite-admin +``` -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 return "not available" errors +- WordPress Advanced database/system tools are unavailable +- All REST API tools (bulk operations, content management) work normally -Without Docker socket, WP-CLI tools will return a "not available" message but all REST API tools work normally. +### Environment Variable Reference -### 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` | +All site configuration follows the pattern: `{PLUGIN_TYPE}_{SITE_ID}_{CONFIG_KEY}=value` - `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`) +- Multiple sites: change `SITE1` to `SITE2`, `SITE3`, etc. + +#### WordPress +```env +WORDPRESS_SITE1_URL=https://your-site.com # Required +WORDPRESS_SITE1_USERNAME=admin # Required +WORDPRESS_SITE1_APP_PASSWORD=xxxx xxxx xxxx xxxx # Required (WordPress Application Password) +WORDPRESS_SITE1_ALIAS=my-site # Recommended +WORDPRESS_SITE1_CONTAINER=wp-container # Optional (for WP-CLI) +``` + +#### WooCommerce +```env +WOOCOMMERCE_SITE1_URL=https://your-site.com # Required +WOOCOMMERCE_SITE1_CONSUMER_KEY=ck_xxx # Required +WOOCOMMERCE_SITE1_CONSUMER_SECRET=cs_xxx # Required +WOOCOMMERCE_SITE1_ALIAS=my-shop # Recommended +``` + +#### WordPress Advanced +```env +WORDPRESS_ADVANCED_SITE1_URL=https://your-site.com # Required +WORDPRESS_ADVANCED_SITE1_USERNAME=admin # Required +WORDPRESS_ADVANCED_SITE1_APP_PASSWORD=xxxx xxxx xxxx xxxx # Required +WORDPRESS_ADVANCED_SITE1_CONTAINER=wp-container # Required (for WP-CLI tools) +WORDPRESS_ADVANCED_SITE1_ALIAS=my-site-admin # Recommended +``` + +#### Gitea +```env +GITEA_SITE1_URL=https://gitea.example.com # Required +GITEA_SITE1_TOKEN=your-access-token # Required +GITEA_SITE1_ALIAS=my-gitea # Recommended +``` + +#### n8n +```env +N8N_SITE1_URL=https://n8n.example.com # Required +N8N_SITE1_API_KEY=your-api-key # Required +N8N_SITE1_ALIAS=my-n8n # Recommended +``` + +#### Supabase +```env +SUPABASE_SITE1_URL=https://your-project.supabase.co # Required +SUPABASE_SITE1_API_KEY=your-service-role-key # Required +SUPABASE_SITE1_ALIAS=my-supabase # Recommended +``` + +#### OpenPanel +```env +OPENPANEL_SITE1_URL=https://openpanel.example.com # Required +OPENPANEL_SITE1_CLIENT_ID=your-client-id # Required +OPENPANEL_SITE1_CLIENT_SECRET=your-client-secret # Required +OPENPANEL_SITE1_ALIAS=my-analytics # Recommended +``` + +#### Appwrite +```env +APPWRITE_SITE1_URL=https://appwrite.example.com # Required +APPWRITE_SITE1_API_KEY=your-api-key # Required +APPWRITE_SITE1_PROJECT_ID=your-project-id # Required +APPWRITE_SITE1_ALIAS=my-appwrite # Recommended +``` + +#### Directus +```env +DIRECTUS_SITE1_URL=https://directus.example.com # Required +DIRECTUS_SITE1_TOKEN=your-admin-token # Required +DIRECTUS_SITE1_ALIAS=my-directus # Recommended +``` + +> **Note:** Use `APP_PASSWORD` (WordPress Application Password), not `PASSWORD`. ### Configuration Tips @@ -286,7 +360,13 @@ 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. +All requests require **Bearer token** authentication via the `Authorization` header: + +``` +Authorization: Bearer YOUR_API_KEY +``` + +> `X-API-Key` header and query parameter auth are **not** supported. ### Claude Desktop @@ -295,8 +375,9 @@ Add to `claude_desktop_config.json`: ```json { "mcpServers": { - "mcphub": { - "url": "https://your-server:8000/mcp", + "mcphub-wordpress": { + "type": "streamableHttp", + "url": "http://your-server:8000/wordpress/mcp", "headers": { "Authorization": "Bearer YOUR_MASTER_API_KEY" } @@ -312,9 +393,9 @@ Add to `.mcp.json` in your project: ```json { "mcpServers": { - "mcphub": { - "type": "sse", - "url": "https://your-server:8000/mcp", + "mcphub-wordpress": { + "type": "http", + "url": "http://your-server:8000/wordpress/mcp", "headers": { "Authorization": "Bearer YOUR_MASTER_API_KEY" } @@ -327,8 +408,8 @@ Add to `.mcp.json` in your project: Go to **Settings > MCP Servers > Add Server**: -- **Name**: MCP Hub -- **URL**: `https://your-server:8000/mcp` +- **Name**: MCP Hub WordPress +- **URL**: `http://your-server:8000/wordpress/mcp` - **Headers**: `Authorization: Bearer YOUR_MASTER_API_KEY` ### VS Code + Copilot @@ -338,9 +419,9 @@ Add to `.vscode/mcp.json`: ```json { "servers": { - "mcphub": { - "type": "sse", - "url": "https://your-server:8000/mcp", + "mcphub-wordpress": { + "type": "http", + "url": "http://your-server:8000/wordpress/mcp", "headers": { "Authorization": "Bearer YOUR_MASTER_API_KEY" } @@ -357,6 +438,8 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au 2. In ChatGPT, add MCP server: `https://your-server:8000/mcp` 3. ChatGPT auto-discovers OAuth metadata and registers +> **Important**: Use `"type": "streamableHttp"` for Claude Desktop and `"type": "http"` for VS Code/Claude Code. Using `"type": "sse"` will cause `400 Bad Request` errors. + --- ## Using MCP Tools @@ -391,31 +474,40 @@ The `site` parameter accepts either a **site_id** (e.g., `site1`) or an **alias* ### Multi-Endpoint Architecture -Use specific endpoints to limit tool access and save tokens: +Use the most specific endpoint for your use case to minimize token usage: -``` -/mcp → All 596 tools (Master API Key) -/system/mcp → System tools only (24 tools) -/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) -``` +| Endpoint | Use Case | Tools | `site` param? | +|----------|----------|------:|:-------------:| +| `/project/{alias}/mcp` | Single-site workflow | 22-100 | No (pre-scoped) | +| `/{plugin}/mcp` | Multi-site management | 23-101 | Yes | +| `/system/mcp` | System administration | 24 | N/A | +| `/mcp` | Admin & discovery only | 596 | Yes | -> **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. +> **Recommendation**: Always use the most specific endpoint. Using `/mcp` (596 tools) wastes context tokens and degrades AI response quality. + +**Available plugin endpoints:** + +| Endpoint | Plugin | Tools | +|----------|--------|------:| +| `/wordpress/mcp` | WordPress | 67 | +| `/woocommerce/mcp` | WooCommerce | 28 | +| `/wordpress-advanced/mcp` | WordPress Advanced | 22 | +| `/gitea/mcp` | Gitea | 56 | +| `/n8n/mcp` | n8n | 56 | +| `/supabase/mcp` | Supabase | 70 | +| `/openpanel/mcp` | OpenPanel | 73 | +| `/appwrite/mcp` | Appwrite | 100 | +| `/directus/mcp` | Directus | 100 | +| `/system/mcp` | System Management | 24 | **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 | +| Feature | Plugin (`/wordpress/mcp`) | Project (`/project/{alias}/mcp`) | +|---------|:------------------------:|:-------------------------------:| +| `list_sites` tool | Yes | No | +| `site` parameter needed | Yes | No (pre-scoped) | +| Tool count | N + 1 (includes `list_sites`) | N | +| Multi-site support | Yes | No (single site) | --- diff --git a/plugins/wordpress_advanced/plugin.py b/plugins/wordpress_advanced/plugin.py index 703ea2d..dd5bbcc 100644 --- a/plugins/wordpress_advanced/plugin.py +++ b/plugins/wordpress_advanced/plugin.py @@ -112,22 +112,24 @@ class WordPressAdvancedPlugin(BasePlugin): Dict with health status and WP-CLI availability """ try: - # Test WP-CLI access (primary requirement for wordpress_advanced) - wp_cli_version = await self.system.wp_cli_version() - wp_cli_available = bool(wp_cli_version.get("version")) - - # Test REST API access with a public endpoint + # Test REST API access (primary health indicator — most tools use REST) rest_api_available = False try: - # Use a public endpoint that doesn't require authentication site_info = await self.client.get("/") rest_api_available = bool(site_info.get("name")) except Exception as e: - self.logger.warning(f"REST API check failed (non-critical): {e}") - rest_api_available = False + self.logger.warning(f"REST API check failed: {e}") + + # Test WP-CLI access (optional — only needed for database/system tools) + wp_cli_available = False + try: + wp_cli_version = await self.system.wp_cli_version() + wp_cli_available = bool(wp_cli_version.get("version")) + except Exception: + pass return { - "healthy": wp_cli_available or rest_api_available, + "healthy": 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 791580d..0109802 100644 --- a/server.py +++ b/server.py @@ -1180,25 +1180,29 @@ def add_endpoint_middleware(endpoint_mcp, endpoint_name: str = "unknown"): # Internal implementation functions (not decorated) for system tools async def _list_projects_impl() -> str: - """Internal implementation for listing projects.""" + """Internal implementation for listing projects (reads from SiteManager).""" + import json + 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} + projects = [] - for project in projects: - full_id = project.get("id", "") - site_info = site_lookup.get(full_id, {}) + for site_info in all_sites: 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" + site_id = site_info.get("site_id") + full_id = site_info.get("full_id", "") + path_suffix = alias if alias and alias != site_id else full_id + projects.append( + { + "id": full_id, + "type": site_info.get("plugin_type", ""), + "project_id": site_id, + "alias": alias, + "endpoint": f"/project/{path_suffix}/mcp", + } + ) result = {"total": len(projects), "projects": projects} - import json - return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error listing projects: {e}", exc_info=True) @@ -1269,14 +1273,22 @@ async def get_project_info(project_id: str) -> str: Returns: JSON string with project information """ + import json + try: + # Try legacy ProjectManager first info = project_manager.get_project_info(project_id) + # Fallback to SiteManager if legacy finds nothing + if info is None: + for site_data in site_manager.list_all_sites(): + if site_data["full_id"] == project_id: + info = site_data + break + 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) @@ -3111,7 +3123,6 @@ async def health_check(request: Request) -> JSONResponse: { "status": "healthy", "uptime": uptime_seconds, - "projects": len(project_manager.projects), "sites": site_manager.get_count(), "tools": _total_tool_count, # Total tools (plugin + system) "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), @@ -3480,12 +3491,17 @@ Use get_endpoints() to see all available MCP endpoints.""" @system_mcp.tool() async def get_project_info(project_id: str) -> str: """Get detailed information about a specific project.""" + import json + try: info = project_manager.get_project_info(project_id) + if info is None: + for site_data in site_manager.list_all_sites(): + if site_data["full_id"] == project_id: + info = site_data + break 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) @@ -4507,10 +4523,10 @@ def main(): logger.info(f"Host: {args.host}") logger.info(f"Port: {args.port}") - # Check if any projects were discovered - if len(project_manager.projects) == 0: - logger.warning("No projects discovered! Check your environment variables.") - logger.warning("Expected format: {PLUGIN_TYPE}_{PROJECT_ID}_{CONFIG_KEY}=value") + # Check if any sites were discovered (SiteManager is the primary discovery system) + if site_manager.get_count() == 0: + logger.warning("No sites discovered! Check your environment variables.") + logger.warning("Expected format: {PLUGIN_TYPE}_{SITE_ID}_{CONFIG_KEY}=value") logger.warning("Example: WORDPRESS_SITE1_URL=https://example.com") try: