23 Commits

Author SHA1 Message Date
airano
5d97b31c12 fix: auto-load .env file for PyPI installations
Some checks failed
Release / Test before release (push) Has been cancelled
Release / Publish to PyPI (push) Has been cancelled
Release / Publish to Docker Hub (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
PyPI users running `mcphub` had to manually export env vars because
server.py didn't call load_dotenv(). Docker users were unaffected
since docker-compose loads .env automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 03:59:26 +03:30
airano
72cfdad5e3 style: fix black formatting in health.py
Some checks failed
Release / Test before release (push) Has been cancelled
Release / Publish to PyPI (push) Has been cancelled
Release / Publish to Docker Hub (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 03:40:08 +03:30
airano
5ce70f120e chore: bump version to 3.0.2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 03:38:13 +03:30
airano
cafcd64eff fix(health): resolve "Site not found" for wordpress_advanced health checks
The health monitor's _basic_site_health_check() used split("_", 1) to parse
project IDs like "wordpress_advanced_site1", incorrectly yielding
plugin_type="wordpress" and site_id="advanced_site1". Now uses SiteManager's
list_all_sites() for proper full_id lookup, and creates a temporary plugin
instance for proper health checks (REST API, WP-CLI) instead of basic HTTP.

Also fixes the same split bug in SiteManager.get_effective_path_suffix().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 03:29:05 +03:30
airano
d74594524a docs(readme): clarify OAuth as optional, document CONTAINER env var
Some checks failed
Release / Test before release (push) Has been cancelled
Release / Publish to PyPI (push) Has been cancelled
Release / Publish to Docker Hub (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:52:44 +03:30
airano
b6551abe0a docs: clarify OAuth as optional, document CONTAINER env var
- docker-compose: OAuth vars commented out by default
- env.example: added WordPress Advanced section with CONTAINER docs
- DOCKER_README: added CONTAINER to env var table with usage notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:46:53 +03:30
airano
cb2a835d0e fix: revert docker-compose ports for standalone users, fix endpoint path
- docker-compose: ports "8000:8000" active by default
- README: fix /wordpress_advanced → /wordpress-advanced
- DOCKER_README: add version 3.0.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:32:08 +03:30
airano
bb2a416dd5 fix(qa): health check, admin notice, docs, logo, Coolify port
- WP Advanced health_check uses aiohttp to /wp-json/ directly
- SEO Bridge admin notice only on Plugins page
- SEO Bridge README.md consolidated, changelog complete
- OpenPanel Self-Hosted README.md created
- Dashboard sidebar uses actual logo.svg
- docker-compose: expose instead of ports for Coolify
- README: version badge, wordpress_advanced endpoint, env var table
- Docs: SSE references fixed to streamable-http

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:55:29 +03:30
airano
ff79968170 feat(wp): rename OpenPanel plugin to OpenPanel Self-Hosted for WP.org
- Separate plugin identity for WordPress.org submission
- Text domain, folder, and file renamed to openpanel-self-hosted
- Description updated as fork of official OpenPanel plugin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:57:48 +03:30
airano
bb851e4be2 fix(qa): health monitor SiteManager, WP plugins, favicon, download ZIPs
- Health monitor now uses SiteManager for complete site discovery
- SEO Bridge: GPLv2+ license, Tested up to 6.9, fix escaping
- OpenPanel: fix wp_unslash, reduce tags, update branding
- Add logo.svg favicon, static file serving, plugin ZIPs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:11:49 +03:30
airano
91979ee18d fix(qa): SEO API Bridge fixes, transport type corrections, README improvements
- Fix SEO API Bridge branding, meta keys, route namespaces, GET auth
- Create WordPress.org-compatible readme.txt
- Fix transport types across all docs (streamableHttp/http, not sse)
- Add WordPress Plugin Requirements section to README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 19:42:32 +03:30
airano
809dc264af fix(qa): resolve remaining v3.0.1 issues — CI, code bugs, docs
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 <noreply@anthropic.com>
2026-02-18 18:55:12 +03:30
airano
cb6bcd8136 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 <noreply@anthropic.com>
2026-02-18 18:29:15 +03:30
airano
3b87c01d78 fix(discovery): prevent WORDPRESS_ADVANCED_ prefix collision with WORDPRESS_
ProjectManager and SiteManager now skip env vars that belong to a more
specific plugin type (e.g. WORDPRESS_ADVANCED_*) when discovering sites
for a shorter prefix (WORDPRESS_). This prevents ghost projects, ERROR
logs, and false health alerts on startup.

Also downgrades Docker socket errors in WP-CLI to warnings — if Docker
socket is not mounted, WP-CLI features are silently unavailable instead
of logging ERROR and triggering health alerts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 17:08:34 +03:30
airano
85379ec36c fix(security): validate Bearer token value in MCP endpoints, not just presence
Previously any Bearer token (even invalid) passed the HTTP middleware and
got a session with access to tools/list and resources/list. Now the
OAuthRequiredMiddleware validates tokens against master key, API keys,
and OAuth JWT before allowing initialize.

Also fixes:
- WordPress Advanced env prefix: WORDPRESS_ → WORDPRESS_ADVANCED_ in docs
- Bump version to 3.0.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:24:04 +03:30
airano
4a45178d2d fix(auth): support auto-generated temp key in dashboard login
When no MASTER_API_KEY env var is set, AuthManager generates a temp key
but DashboardAuth didn't know about it. Now validate_api_key() also
checks AuthManager as a fallback, so temp key login works for first-time
Docker users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 15:56:19 +03:30
airano
f303a04322 fix(auth): fix dashboard login with project API keys
validate_key() requires project_id argument but was called without it
in dashboard auth. Fixed by passing skip_project_check=True.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 15:43:05 +03:30
airano
bb74703311 fix(ux): improve new user onboarding experience
- Show full temporary API key in logs (not masked) so users can log in
- Redirect root URL / to /dashboard (was 404)
- Add setup guidance and docs link to login page
- Make footer version dynamic (read from pyproject.toml)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 03:41:04 +03:30
airano
aa8afce707 fix: correct tool count (596), default English dashboard, clean up stale references
- Tool count corrected: 589→596 (was missing 4 OAuth + 4 system config tools)
- System tools: 17→24 (accurate count of @mcp.tool() functions)
- Dashboard defaults to English; Farsi only via ?lang=fa query param
- Updated across all files: README, CLAUDE.md, CONTRIBUTING, DOCKER_README,
  getting-started, CHANGELOG, endpoints config
- Removed remaining Phase references from server.py startup log
- Tests: 289 passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 03:14:28 +03:30
airano
076a0b940e fix(server): resolve lint error and clean up production logs
- Fix F821 undefined name `core` by using local import in fallback path
- Remove all internal terminology from logs (Phase X, Option B)
- Clean Phase/Option B references from tool docstrings (visible to MCP clients)
- Suppress websockets DeprecationWarning (noisy, harmless)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:59:48 +03:30
airano
7daf604a32 fix(packaging): move templates into core/ for PyPI distribution
Templates were at project root (templates/) which is outside the Python
package, so `pip install mcphub-server` would not include them. Moved to
core/templates/ and updated path references in server.py and
core/dashboard/routes.py. Also added py-modules for server.py and
server_multi.py to pyproject.toml.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:41:28 +03:30
airano
5e0441c85e docs: add env.example template for new users
Previously excluded from community edition by mistake.
This template has placeholder values only (no real secrets)
and is needed for the Quick Start: `cp env.example .env`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:23:46 +03:30
airano
3a3f04aca8 docs(onboarding): improve new user experience + fix OpenPanel tools
- Add DOCKER_README.md for Docker Hub overview page
- Fix docker-compose.yaml port mapping for local users ("8000:8000")
- Add "Verify It Works" section with dashboard URL to README
- Restructure env.example with clear REQUIRED section
- Add post-Docker verification checklist to getting-started.md
- Fix 4 OpenPanel tools: parameter ordering (non-default before default)
- Rebrand all "Coolify Projects" to "MCP Hub" in server and Dockerfile
- Make MCP namespace prefix stripping generic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:22:48 +03:30
50 changed files with 1706 additions and 665 deletions

View File

@@ -30,7 +30,7 @@ After 2-month hiatus (Dec 2025 - Feb 2026), updated all dependencies and verifie
- All 54 tests now pass (previously 5 were failing) - All 54 tests now pass (previously 5 were failing)
#### Verified #### Verified
- All 589 tools generate correctly - All 596 tools generate correctly
- Middleware API stable (Middleware, MiddlewareContext, get_http_headers) - Middleware API stable (Middleware, MiddlewareContext, get_http_headers)
- 30+ custom routes operational (dashboard + OAuth) - 30+ custom routes operational (dashboard + OAuth)
- Multi-endpoint architecture functional - Multi-endpoint architecture functional

View File

@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
**MCP Hub** — a Python MCP (Model Context Protocol) server that manages multiple self-hosted projects through a unified plugin architecture. Supports 9 plugin types (WordPress, WooCommerce, WordPress Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~589 tools total. The tool count stays constant regardless of how many sites are configured. **MCP Hub** — a Python MCP (Model Context Protocol) server that manages multiple self-hosted services through a unified plugin architecture. Supports 9 plugin types (WordPress, WooCommerce, WordPress Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with 596 tools total. The tool count stays constant regardless of how many sites are configured.
## Quick Setup ## Quick Setup
@@ -12,7 +12,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
cp env.example .env # Copy and fill in credentials cp env.example .env # Copy and fill in credentials
pip install -e ".[dev]" # Install with dev deps pip install -e ".[dev]" # Install with dev deps
python server.py # Run (stdio) or: python server.py # Run (stdio) or:
python server.py --transport sse --port 8000 # Run (HTTP) python server.py --transport streamable-http --port 8000 # Run (HTTP)
``` ```
## Build & Development Commands ## Build & Development Commands
@@ -24,8 +24,8 @@ pip install -e ".[dev]"
# Run server (stdio transport for Claude Desktop) # Run server (stdio transport for Claude Desktop)
python server.py python server.py
# Run server (SSE/HTTP transport for testing) # Run server (HTTP transport for testing)
python server.py --transport sse --port 8000 python server.py --transport streamable-http --port 8000
# Run all tests # Run all tests
pytest pytest
@@ -72,7 +72,7 @@ All configured in `pyproject.toml`:
├── server_multi.py # Alternative multi-endpoint server ├── server_multi.py # Alternative multi-endpoint server
├── core/ # Layer 1: Core system modules ├── core/ # Layer 1: Core system modules
├── plugins/ # Layer 2: Plugin system (9 plugins) ├── plugins/ # Layer 2: Plugin system (9 plugins)
├── templates/ # Jinja2 templates (dashboard + OAuth) ├── core/templates/ # Jinja2 templates (dashboard + OAuth)
├── tests/ # Organized test suite ├── tests/ # Organized test suite
├── scripts/ # Setup & deployment scripts ├── scripts/ # Setup & deployment scripts
├── wordpress-plugin/ # Companion WP plugins (PHP) ├── wordpress-plugin/ # Companion WP plugins (PHP)
@@ -181,7 +181,7 @@ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
- `server_multi.py` is the alternative multi-endpoint entry point; `server.py` is the primary - `server_multi.py` is the alternative multi-endpoint entry point; `server.py` is the primary
- `wordpress-plugin/` contains companion WP plugins (openpanel, seo-api-bridge) — these are PHP, not Python - `wordpress-plugin/` contains companion WP plugins (openpanel, seo-api-bridge) — these are PHP, not Python
- `env.example` has "FUTURE" labels for Supabase/Gitea but both are fully implemented - `env.example` has "FUTURE" labels for Supabase/Gitea but both are fully implemented
- Dashboard templates live in `templates/` (not inside `core/dashboard/`) - Dashboard templates live in `core/templates/` (included in pip package as `package_data`)
- `ruff` config uses top-level `select` key in pyproject.toml (not `[tool.ruff.lint]` nested format) - `ruff` config uses top-level `select` key in pyproject.toml (not `[tool.ruff.lint]` nested format)
- The `scripts/` directory has platform-specific setup scripts: `setup.sh` (Linux/Mac), `setup.ps1` (Windows) - The `scripts/` directory has platform-specific setup scripts: `setup.sh` (Linux/Mac), `setup.ps1` (Windows)

View File

@@ -20,7 +20,7 @@ pytest # Verify setup
```bash ```bash
python server.py # stdio (Claude Desktop) python server.py # stdio (Claude Desktop)
python server.py --transport sse --port 8000 # HTTP (testing) python server.py --transport streamable-http --port 8000 # HTTP (testing)
``` ```
### Code Style ### Code Style
@@ -119,8 +119,8 @@ Then register in `plugins/__init__.py` and add tests.
``` ```
core/ # Core system (auth, site manager, tool registry, dashboard) core/ # Core system (auth, site manager, tool registry, dashboard)
plugins/ # Plugin system (9 plugins, each with handlers + schemas) plugins/ # Plugin system (9 plugins, each with handlers + schemas)
templates/ # Jinja2 templates (dashboard + OAuth) core/templates/ # Jinja2 templates (dashboard + OAuth)
tests/ # Test suite (289 tests) tests/ # Test suite (290 tests)
scripts/ # Setup & deployment scripts scripts/ # Setup & deployment scripts
docs/ # Documentation docs/ # Documentation
``` ```

175
DOCKER_README.md Normal file
View File

@@ -0,0 +1,175 @@
# MCP Hub
**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.
## Quick Start
### 1. Create a `.env` file
```bash
# Recommended (auto-generates temp key if omitted — check container logs)
MASTER_API_KEY=your-secure-key-here
# Add at least one WordPress site
WORDPRESS_SITE1_URL=https://your-site.com
WORDPRESS_SITE1_USERNAME=admin
WORDPRESS_SITE1_APP_PASSWORD=xxxx xxxx xxxx xxxx
WORDPRESS_SITE1_ALIAS=mysite
```
### 2. Run the container
```bash
docker run -d \
--name mcphub \
-p 8000:8000 \
--env-file .env \
airano/mcphub:latest
```
### 3. Verify it works
```bash
# Health check (wait ~30 seconds for startup)
curl http://localhost:8000/health
# Open the web dashboard
# http://localhost:8000/dashboard
```
### 4. Connect your AI client
**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"mcphub": {
"type": "streamableHttp",
"url": "http://localhost:8000/wordpress/mcp",
"headers": {
"Authorization": "Bearer your-secure-key-here"
}
}
}
}
```
**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:
```
Authorization: Bearer YOUR_API_KEY
```
> `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
```yaml
services:
mcphub:
image: airano/mcphub:latest
ports:
- "8000:8000"
env_file:
- .env
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:
mcphub-data:
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
```
## After Starting
| URL | Description |
|-----|-------------|
| `http://localhost:8000/health` | Health check & status |
| `http://localhost:8000/dashboard` | Web dashboard (manage API keys, view sites, health) |
| `http://localhost:8000/mcp` | MCP endpoint (connect AI clients here) |
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `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 |
| `WORDPRESS_SITE1_ALIAS` | Recommended | Friendly name (e.g., `myblog`) |
| `WORDPRESS_SITE1_CONTAINER` | For WP-CLI | Docker container name of your WordPress site (enables cache/db/system tools) |
| `OAUTH_JWT_SECRET_KEY` | For OAuth | JWT secret for ChatGPT auto-registration (not needed for Claude/Cursor) |
| `OAUTH_BASE_URL` | For OAuth | Public URL of your server (not needed for Claude/Cursor) |
> **CONTAINER**: Required for WP-CLI tools (cache flush, database export, system info) and **all** WordPress Advanced tools. Find your container name: `docker ps --filter name=wordpress`. Also requires Docker socket mount.
> **OAuth**: Only needed for ChatGPT Remote MCP auto-registration. For Claude Desktop, Claude Code, Cursor, and VS Code — just use `MASTER_API_KEY` with Bearer token auth.
Add more sites with `SITE2`, `SITE3`, etc. See [full configuration guide](https://github.com/airano-ir/mcphub/blob/main/docs/getting-started.md).
## Supported Plugins
| Plugin | Tools | Env Prefix |
|--------|-------|------------|
| WordPress | 67 | `WORDPRESS_` |
| WooCommerce | 28 | `WOOCOMMERCE_` |
| WordPress Advanced | 22 | `WORDPRESS_ADVANCED_` |
| Gitea | 56 | `GITEA_` |
| n8n | 56 | `N8N_` |
| Supabase | 70 | `SUPABASE_` |
| OpenPanel | 73 | `OPENPANEL_` |
| Appwrite | 100 | `APPWRITE_` |
| Directus | 100 | `DIRECTUS_` |
## Links
- **GitHub**: [github.com/airano-ir/mcphub](https://github.com/airano-ir/mcphub)
- **PyPI**: [pypi.org/project/mcphub-server](https://pypi.org/project/mcphub-server/)
- **Documentation**: [Getting Started Guide](https://github.com/airano-ir/mcphub/blob/main/docs/getting-started.md)
- **License**: MIT

View File

@@ -1,5 +1,5 @@
# =================================== # ===================================
# Coolify Projects MCP Server - Dockerfile # MCP Hub — Dockerfile
# =================================== # ===================================
# Multi-stage build for optimized image size # Multi-stage build for optimized image size
# Production-ready with security best practices # Production-ready with security best practices

161
README.md
View File

@@ -6,12 +6,13 @@
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.
[![Version: 3.0.1](https://img.shields.io/badge/version-3.0.1-blue.svg)](https://github.com/airano-ir/mcphub/releases)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-3776ab.svg)](https://www.python.org/) [![Python 3.11+](https://img.shields.io/badge/python-3.11+-3776ab.svg)](https://www.python.org/)
[![PyPI](https://img.shields.io/pypi/v/mcphub-server.svg)](https://pypi.org/project/mcphub-server/) [![PyPI](https://img.shields.io/pypi/v/mcphub-server.svg)](https://pypi.org/project/mcphub-server/)
[![Docker](https://img.shields.io/docker/v/airano/mcphub?label=docker)](https://hub.docker.com/r/airano/mcphub) [![Docker](https://img.shields.io/docker/v/airano/mcphub?label=docker)](https://hub.docker.com/r/airano/mcphub)
[![Tests: 289 passing](https://img.shields.io/badge/tests-289%20passing-brightgreen.svg)]() [![Tests: 290 passing](https://img.shields.io/badge/tests-290%20passing-brightgreen.svg)]()
[![Tools: 589](https://img.shields.io/badge/tools-589-orange.svg)]() [![Tools: 596](https://img.shields.io/badge/tools-596-orange.svg)]()
[![CI](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml/badge.svg)](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml) [![CI](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml/badge.svg)](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml)
</div> </div>
@@ -46,7 +47,7 @@ MCP Hub is the first MCP server that lets you manage WordPress, WooCommerce, and
--- ---
## 589 Tools Across 9 Plugins ## 596 Tools Across 9 Plugins
| Plugin | Tools | What You Can Do | | Plugin | Tools | What You Can Do |
|--------|-------|-----------------| |--------|-------|-----------------|
@@ -59,8 +60,8 @@ MCP Hub is the first MCP server that lets you manage WordPress, WooCommerce, and
| **OpenPanel** | 73 | Events, funnels, profiles, dashboards, projects | | **OpenPanel** | 73 | Events, funnels, profiles, dashboards, projects |
| **Appwrite** | 100 | Databases, auth, storage, functions, teams, messaging | | **Appwrite** | 100 | Databases, auth, storage, functions, teams, messaging |
| **Directus** | 100 | Collections, items, users, files, flows, permissions | | **Directus** | 100 | Collections, items, users, files, flows, permissions |
| **System** | 17 | Health monitoring, API keys, project discovery | | **System** | 24 | Health monitoring, API keys, OAuth management, audit |
| **Total** | **589** | Constant count — scales to unlimited sites | | **Total** | **596** | Constant count — scales to unlimited sites |
--- ---
@@ -72,14 +73,15 @@ MCP Hub is the first MCP server that lets you manage WordPress, WooCommerce, and
git clone https://github.com/airano-ir/mcphub.git git clone https://github.com/airano-ir/mcphub.git
cd mcphub cd mcphub
cp env.example .env cp env.example .env
# Edit .env with your site credentials # Edit .env — set MASTER_API_KEY and add your site credentials
docker compose up -d docker compose up -d
``` ```
### Option 2: PyPI ### Option 2: Docker Hub (No Clone)
```bash ```bash
pip install mcphub-server # Create a .env file with your credentials (see "Configure Your Sites" below)
docker run -d --name mcphub -p 8000:8000 --env-file .env airano/mcphub:latest
``` ```
### Option 3: From Source ### Option 3: From Source
@@ -90,15 +92,28 @@ cd mcphub
pip install -e . pip install -e .
cp env.example .env cp env.example .env
# Edit .env with your site credentials # Edit .env with your site credentials
python server.py --transport sse --port 8000 python server.py --transport streamable-http --port 8000
``` ```
### Verify It Works
After starting the server, wait ~30 seconds then:
```bash
# Check server health
curl http://localhost:8000/health
```
Open the **web dashboard** in your browser: **http://localhost:8000/dashboard**
You should see the login page. Use your `MASTER_API_KEY` to log in.
### Configure Your Sites ### Configure Your Sites
Add site credentials to `.env`: Add site credentials to `.env`:
```bash ```bash
# Master API Key (required) # Master API Key (recommended — auto-generates temp key if omitted)
MASTER_API_KEY=your-secure-key-here MASTER_API_KEY=your-secure-key-here
# WordPress Site # WordPress Site
@@ -119,8 +134,63 @@ GITEA_REPO1_TOKEN=your_gitea_token
GITEA_REPO1_ALIAS=mygitea GITEA_REPO1_ALIAS=mygitea
``` ```
<details>
<summary><b>Full Environment Variable Reference</b></summary>
**System Configuration:**
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `MASTER_API_KEY` | Recommended | Auto-generated | Master API key for admin access |
| `LOG_LEVEL` | No | `INFO` | Logging level (DEBUG, INFO, WARNING, ERROR) |
| `OAUTH_JWT_SECRET_KEY` | For OAuth | — | JWT secret for ChatGPT auto-registration (not needed for Claude/Cursor) |
| `OAUTH_BASE_URL` | For OAuth | — | Public URL of your server (not needed for Claude/Cursor) |
| `OAUTH_JWT_ALGORITHM` | No | `HS256` | JWT algorithm |
| `OAUTH_ACCESS_TOKEN_TTL` | No | `3600` | Access token TTL in seconds |
| `OAUTH_REFRESH_TOKEN_TTL` | No | `604800` | Refresh token TTL in seconds |
| `OAUTH_STORAGE_TYPE` | No | `json` | Token storage type |
| `OAUTH_STORAGE_PATH` | No | `/app/data` | Data directory path |
> **OAuth** is only needed for ChatGPT Remote MCP auto-registration. For Claude Desktop, Claude Code, Cursor, and VS Code — just use `MASTER_API_KEY` with Bearer token auth.
**Plugin Site Configuration** — Pattern: `{PLUGIN_TYPE}_{SITE_ID}_{KEY}`
| Plugin | Required Keys | Optional Keys |
|--------|--------------|---------------|
| `WORDPRESS` | `URL`, `USERNAME`, `APP_PASSWORD` | `ALIAS`, `CONTAINER` |
| `WOOCOMMERCE` | `URL`, `CONSUMER_KEY`, `CONSUMER_SECRET` | `ALIAS` |
| `WORDPRESS_ADVANCED` | `URL`, `USERNAME`, `APP_PASSWORD`, `CONTAINER` | `ALIAS` |
| `GITEA` | `URL`, `TOKEN` | `ALIAS` |
| `N8N` | `URL`, `API_KEY` | `ALIAS` |
| `SUPABASE` | `URL`, `SERVICE_ROLE_KEY` | `ALIAS` |
| `OPENPANEL` | `URL`, `CLIENT_ID`, `CLIENT_SECRET` | `ALIAS` |
| `APPWRITE` | `URL`, `API_KEY`, `PROJECT_ID` | `ALIAS` |
| `DIRECTUS` | `URL`, `TOKEN` | `ALIAS` |
> **CONTAINER**: Docker container name of your WordPress site. Optional for WordPress (enables WP-CLI tools like cache flush, transient management). **Required** for WordPress Advanced (all 22 tools use WP-CLI). Find your container: `docker ps --filter name=wordpress`. Also requires Docker socket mount.
**Example** — Multiple WordPress sites:
```bash
WORDPRESS_BLOG_URL=https://blog.example.com
WORDPRESS_BLOG_USERNAME=admin
WORDPRESS_BLOG_APP_PASSWORD=xxxx xxxx xxxx xxxx
WORDPRESS_BLOG_ALIAS=blog
WORDPRESS_SHOP_URL=https://shop.example.com
WORDPRESS_SHOP_USERNAME=admin
WORDPRESS_SHOP_APP_PASSWORD=yyyy yyyy yyyy yyyy
WORDPRESS_SHOP_ALIAS=shop
```
</details>
### Connect Your AI Client ### Connect Your AI Client
All MCP clients use **Bearer token** authentication: `Authorization: Bearer YOUR_API_KEY`
> Use a plugin-specific endpoint (e.g., `/wordpress/mcp`) instead of `/mcp` to reduce tool count and save tokens. See [Architecture](#architecture) below.
<details> <details>
<summary><b>Claude Desktop</b></summary> <summary><b>Claude Desktop</b></summary>
@@ -129,10 +199,11 @@ Add to `claude_desktop_config.json`:
```json ```json
{ {
"mcpServers": { "mcpServers": {
"mcphub": { "mcphub-wordpress": {
"url": "https://your-server:8000/mcp", "type": "streamableHttp",
"url": "http://your-server:8000/wordpress/mcp",
"headers": { "headers": {
"Authorization": "Bearer YOUR_MASTER_API_KEY" "Authorization": "Bearer YOUR_API_KEY"
} }
} }
} }
@@ -149,11 +220,11 @@ Add to `.mcp.json` in your project:
```json ```json
{ {
"mcpServers": { "mcpServers": {
"mcphub": { "mcphub-wordpress": {
"type": "sse", "type": "http",
"url": "https://your-server:8000/mcp", "url": "http://your-server:8000/wordpress/mcp",
"headers": { "headers": {
"Authorization": "Bearer YOUR_MASTER_API_KEY" "Authorization": "Bearer YOUR_API_KEY"
} }
} }
} }
@@ -167,9 +238,9 @@ Add to `.mcp.json` in your project:
Go to **Settings > MCP Servers > Add Server**: Go to **Settings > MCP Servers > Add Server**:
- **Name**: MCP Hub - **Name**: MCP Hub WordPress
- **URL**: `https://your-server:8000/mcp` - **URL**: `http://your-server:8000/wordpress/mcp`
- **Headers**: `Authorization: Bearer YOUR_MASTER_API_KEY` - **Headers**: `Authorization: Bearer YOUR_API_KEY`
</details> </details>
@@ -181,11 +252,11 @@ Add to `.vscode/mcp.json`:
```json ```json
{ {
"servers": { "servers": {
"mcphub": { "mcphub-wordpress": {
"type": "sse", "type": "http",
"url": "https://your-server:8000/mcp", "url": "http://your-server:8000/wordpress/mcp",
"headers": { "headers": {
"Authorization": "Bearer YOUR_MASTER_API_KEY" "Authorization": "Bearer YOUR_API_KEY"
} }
} }
} }
@@ -205,15 +276,18 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
</details> </details>
> **Transport types**: Use `"type": "streamableHttp"` for Claude Desktop and `"type": "http"` for VS Code/Claude Code. Using `"type": "sse"` will cause `400 Bad Request` errors.
--- ---
## Architecture ## Architecture
``` ```
/mcp → Admin endpoint (all 589 tools) /mcp → Admin endpoint (all 596 tools)
/system/mcp → System tools only (17 tools) /system/mcp → System tools only (24 tools)
/wordpress/mcp → WordPress tools (67 tools) /wordpress/mcp → WordPress tools (67 tools)
/woocommerce/mcp → WooCommerce tools (28 tools) /woocommerce/mcp → WooCommerce tools (28 tools)
/wordpress-advanced/mcp → WordPress Advanced tools (22 tools)
/gitea/mcp → Gitea tools (56 tools) /gitea/mcp → Gitea tools (56 tools)
/n8n/mcp → n8n tools (56 tools) /n8n/mcp → n8n tools (56 tools)
/supabase/mcp → Supabase tools (70 tools) /supabase/mcp → Supabase tools (70 tools)
@@ -223,7 +297,13 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
/project/{alias}/mcp → Per-project endpoint (auto-injects site) /project/{alias}/mcp → Per-project endpoint (auto-injects site)
``` ```
**Multi-endpoint architecture**: Give each team member or AI agent access to only the tools they need. **Recommendation**: Use plugin-specific endpoints instead of `/mcp` (596 tools) to minimize token usage.
| Endpoint | Use Case | Tools |
|----------|----------|------:|
| `/project/{alias}/mcp` | Single-site workflow (recommended) | 22-100 |
| `/{plugin}/mcp` | Multi-site management | 23-101 |
| `/mcp` | Admin & discovery only | 596 |
### Security ### Security
@@ -235,6 +315,29 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
> **Compatibility Note**: MCP Hub requires FastMCP 2.x (`>=2.14.0,<3.0.0`). FastMCP 3.0 introduced breaking changes and is not yet supported. If you install dependencies manually, ensure you don't upgrade to FastMCP 3.x. > **Compatibility Note**: MCP Hub requires FastMCP 2.x (`>=2.14.0,<3.0.0`). FastMCP 3.0 introduced breaking changes and is not yet supported. If you install dependencies manually, ensure you don't upgrade to FastMCP 3.x.
### WordPress Plugin Requirements
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/) ([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 Self-Hosted](wordpress-plugin/openpanel-self-hosted/) ([Download ZIP](wordpress-plugin/openpanel-self-hosted.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:
```yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
WORDPRESS_SITE1_CONTAINER: your-wp-container-name
```
Without Docker socket, WP-CLI tools return "not available" but all REST API tools work normally.
--- ---
## Documentation ## Documentation
@@ -258,14 +361,14 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
# Install with dev dependencies # Install with dev dependencies
pip install -e ".[dev]" pip install -e ".[dev]"
# Run tests (289 tests) # Run tests (290 tests)
pytest pytest
# Format and lint # Format and lint
black . && ruff check --fix . black . && ruff check --fix .
# Run server locally # Run server locally
python server.py --transport sse --port 8000 python server.py --transport streamable-http --port 8000
``` ```
--- ---

View File

@@ -27,11 +27,14 @@ class AuthManager:
if not self.master_api_key: if not self.master_api_key:
# Generate a random key if not provided (dev mode) # Generate a random key if not provided (dev mode)
self.master_api_key = secrets.token_urlsafe(32) self.master_api_key = secrets.token_urlsafe(32)
self._is_temporary_key = True
logger.warning( logger.warning(
"No MASTER_API_KEY environment variable found. " "No MASTER_API_KEY environment variable found. "
f"Generated temporary key: {self.master_api_key[:8]}***{self.master_api_key[-4:]} " f"Generated temporary key: {self.master_api_key} "
"(set MASTER_API_KEY in .env for production use)" "(set MASTER_API_KEY in .env for production use)"
) )
else:
self._is_temporary_key = False
# Project-specific keys (future feature) # Project-specific keys (future feature)
self.project_keys = {} self.project_keys = {}

View File

@@ -1,7 +1,5 @@
""" """
Dashboard Authentication - Session-based authentication for Web UI. Dashboard Authentication - Session-based authentication for Web UI.
Phase K.1: Core Infrastructure
""" """
import logging import logging
@@ -91,19 +89,33 @@ class DashboardAuth:
if not api_key: if not api_key:
return False, "", None return False, "", None
# Check master API key # Check master API key (from env var)
if self.master_api_key and secrets.compare_digest(api_key, self.master_api_key): if self.master_api_key and secrets.compare_digest(api_key, self.master_api_key):
return True, "master", None return True, "master", None
# Check AuthManager's master key (covers auto-generated temp keys)
try:
from core.auth import get_auth_manager
auth_mgr = get_auth_manager()
if auth_mgr.validate_master_key(api_key):
return True, "master", None
except Exception as e:
logger.debug(f"AuthManager check skipped: {e}")
# Check project API keys with admin scope # Check project API keys with admin scope
try: try:
from core.api_keys import get_api_key_manager from core.api_keys import get_api_key_manager
api_key_manager = get_api_key_manager() api_key_manager = get_api_key_manager()
key_info = api_key_manager.validate_key(api_key) # Dashboard login is not project-specific, so skip project check
if key_info and "admin" in key_info.get("scope", "").split(): # and require admin scope
return True, "api_key", key_info.get("key_id") key_id = api_key_manager.validate_key(
api_key, project_id="*", required_scope="admin", skip_project_check=True
)
if key_id:
return True, "api_key", key_id
except Exception as e: except Exception as e:
logger.warning(f"Error checking API key: {e}") logger.warning(f"Error checking API key: {e}")

View File

@@ -16,10 +16,8 @@ from .auth import get_dashboard_auth
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Templates directory # Templates directory (core/templates/ — one level up from core/dashboard/)
TEMPLATES_DIR = os.path.join( TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates")
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "templates"
)
templates = Jinja2Templates(directory=TEMPLATES_DIR) templates = Jinja2Templates(directory=TEMPLATES_DIR)
# Plugin display names mapping (for special cases like n8n) # Plugin display names mapping (for special cases like n8n)
@@ -159,15 +157,10 @@ DASHBOARD_TRANSLATIONS = {
def detect_language(accept_language: str | None, query_lang: str | None = None) -> str: def detect_language(accept_language: str | None, query_lang: str | None = None) -> str:
"""Detect language from Accept-Language header or query parameter.""" """Detect language from query parameter. Default is English."""
if query_lang and query_lang in DASHBOARD_TRANSLATIONS: if query_lang and query_lang in DASHBOARD_TRANSLATIONS:
return query_lang return query_lang
if accept_language:
# Check for Persian/Farsi
if "fa" in accept_language.lower() or "fa-ir" in accept_language.lower():
return "fa"
return "en" return "en"
@@ -341,6 +334,7 @@ async def dashboard_login_page(request: Request) -> Response:
"t": t, "t": t,
"error": error, "error": error,
"next_url": next_url, "next_url": next_url,
"version": _get_project_version(),
}, },
) )
@@ -1991,6 +1985,19 @@ def get_registered_plugins() -> list:
return plugins return plugins
def _get_project_version() -> str:
"""Read version from pyproject.toml."""
try:
toml_path = os.path.join(os.path.dirname(os.path.dirname(TEMPLATES_DIR)), "pyproject.toml")
with open(toml_path) as f:
for line in f:
if line.strip().startswith("version"):
return line.split("=")[1].strip().strip('"').strip("'")
except Exception:
pass
return "3.0.1"
def get_about_info() -> dict: def get_about_info() -> dict:
"""Get about information.""" """Get about information."""
import sys import sys
@@ -2005,7 +2012,7 @@ def get_about_info() -> dict:
pass pass
return { return {
"version": "1.0.0", "version": _get_project_version(),
"mcp_version": "2024-11-05", "mcp_version": "2024-11-05",
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", "python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
"tools_count": tools_count, "tools_count": tools_count,

View File

@@ -99,7 +99,7 @@ ENDPOINT_CONFIGS = {
# Mounted at "/" → /mcp (FastMCP adds /mcp automatically) # Mounted at "/" → /mcp (FastMCP adds /mcp automatically)
EndpointType.ADMIN: EndpointConfig( EndpointType.ADMIN: EndpointConfig(
path="/", path="/",
name="Coolify Admin", name="MCP Hub Admin",
description="Full administrative access to all tools and plugins", description="Full administrative access to all tools and plugins",
endpoint_type=EndpointType.ADMIN, endpoint_type=EndpointType.ADMIN,
plugin_types=[], # Empty = all plugins plugin_types=[], # Empty = all plugins
@@ -107,7 +107,7 @@ ENDPOINT_CONFIGS = {
allowed_scopes={"admin"}, allowed_scopes={"admin"},
max_tools=400, max_tools=400,
), ),
# System endpoint - system tools only (17 tools) - Phase X.3 # System endpoint - system tools only (24 tools)
# For API key management, OAuth, rate limiting without loading all plugins # For API key management, OAuth, rate limiting without loading all plugins
EndpointType.SYSTEM: EndpointConfig( EndpointType.SYSTEM: EndpointConfig(
path="/system", path="/system",

View File

@@ -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: This module provides comprehensive health monitoring capabilities including:
- Response time tracking - Response time tracking
@@ -9,8 +9,7 @@ This module provides comprehensive health monitoring capabilities including:
- Dependency health checks - Dependency health checks
- System uptime tracking - System uptime tracking
Author: Coolify MCP Team Author: MCP Hub Team
Version: 7.2
""" """
import json import json
@@ -24,6 +23,7 @@ from typing import Any
from core.audit_log import AuditLogger from core.audit_log import AuditLogger
from core.project_manager import ProjectManager from core.project_manager import ProjectManager
from core.site_manager import SiteManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -133,6 +133,7 @@ class HealthMonitor:
audit_logger: AuditLogger | None = None, audit_logger: AuditLogger | None = None,
metrics_retention_hours: int = 24, metrics_retention_hours: int = 24,
max_metrics_per_project: int = 1000, max_metrics_per_project: int = 1000,
site_manager: SiteManager | None = None,
): ):
""" """
Initialize health monitor. Initialize health monitor.
@@ -142,8 +143,10 @@ class HealthMonitor:
audit_logger: Optional audit logger for logging health events audit_logger: Optional audit logger for logging health events
metrics_retention_hours: Hours to retain historical metrics metrics_retention_hours: Hours to retain historical metrics
max_metrics_per_project: Maximum metrics to store per project max_metrics_per_project: Maximum metrics to store per project
site_manager: Optional SiteManager for comprehensive site discovery
""" """
self.project_manager = project_manager self.project_manager = project_manager
self.site_manager = site_manager
self.audit_logger = audit_logger self.audit_logger = audit_logger
self.metrics_retention_hours = metrics_retention_hours self.metrics_retention_hours = metrics_retention_hours
self.max_metrics_per_project = max_metrics_per_project self.max_metrics_per_project = max_metrics_per_project
@@ -172,7 +175,7 @@ class HealthMonitor:
# Request rate tracking (for requests per minute) # Request rate tracking (for requests per minute)
self.request_timestamps: deque = deque(maxlen=1000) self.request_timestamps: deque = deque(maxlen=1000)
logger.info("HealthMonitor initialized (Phase 7.2)") logger.info("HealthMonitor initialized")
def _setup_default_thresholds(self): def _setup_default_thresholds(self):
"""Setup default alert thresholds.""" """Setup default alert thresholds."""
@@ -401,9 +404,17 @@ class HealthMonitor:
start_time = time.time() start_time = time.time()
try: try:
# Get plugin instance # Get plugin instance from ProjectManager
plugin = self.project_manager.projects.get(project_id) 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:
# Site exists in SiteManager but not legacy ProjectManager
# Create a temporary plugin instance for a proper health check
health_result = await self._site_manager_health_check(project_id)
else:
return ProjectHealthStatus( return ProjectHealthStatus(
project_id=project_id, project_id=project_id,
healthy=False, healthy=False,
@@ -413,9 +424,6 @@ class HealthMonitor:
recent_errors=["Project not found"], recent_errors=["Project not found"],
alerts=["CRITICAL: 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 response_time_ms = (time.time() - start_time) * 1000
# Handle both dict and string (JSON) responses # Handle both dict and string (JSON) responses
@@ -495,6 +503,74 @@ class HealthMonitor:
alerts=[f"CRITICAL: Health check failed - {error_msg}"], alerts=[f"CRITICAL: Health check failed - {error_msg}"],
) )
def _find_site_info(self, project_id: str) -> dict[str, Any] | None:
"""Find site info from SiteManager by full_id."""
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:
return {"healthy": False, "message": "SiteManager not available"}
# Look up site info by full_id (handles multi-word plugin types like wordpress_advanced)
site_info = self._find_site_info(project_id)
if not site_info:
return {"healthy": False, "message": f"Site not found: {project_id}"}
plugin_type = site_info["plugin_type"]
site_id = site_info["site_id"]
try:
config = self.site_manager.get_site_config(plugin_type, site_id)
except (KeyError, ValueError):
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
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]: async def check_all_projects_health(self, include_metrics: bool = True) -> dict[str, Any]:
""" """
Check health of all projects. Check health of all projects.
@@ -507,8 +583,14 @@ class HealthMonitor:
""" """
health_statuses = {} 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 # 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) status = await self.check_project_health(project_id, include_metrics)
health_statuses[project_id] = status.to_dict() health_statuses[project_id] = status.to_dict()
@@ -673,7 +755,10 @@ def get_health_monitor() -> HealthMonitor | None:
def initialize_health_monitor( 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: ) -> HealthMonitor:
""" """
Initialize the global health monitor. Initialize the global health monitor.
@@ -681,11 +766,14 @@ def initialize_health_monitor(
Args: Args:
project_manager: Project manager instance project_manager: Project manager instance
audit_logger: Optional audit logger audit_logger: Optional audit logger
site_manager: Optional SiteManager for comprehensive site discovery
**kwargs: Additional configuration options **kwargs: Additional configuration options
Returns: Returns:
HealthMonitor instance HealthMonitor instance
""" """
global _health_monitor 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 return _health_monitor

View File

@@ -139,7 +139,7 @@ class TokenManager:
logger.warning("Expired access token") logger.warning("Expired access token")
raise raise
except jwt.InvalidTokenError as e: except jwt.InvalidTokenError as e:
logger.warning(f"Invalid access token: {e}") logger.debug(f"Invalid access token: {e}")
raise raise
def generate_refresh_token(self, client_id: str, access_token: str) -> str: def generate_refresh_token(self, client_id: str, access_token: str) -> str:

View File

@@ -61,11 +61,24 @@ class ProjectManager:
""" """
prefix = plugin_type.upper() + "_" prefix = plugin_type.upper() + "_"
# Build list of longer prefixes from other plugin types to avoid collisions.
# e.g. WORDPRESS_ must not match WORDPRESS_ADVANCED_ env vars.
plugin_types = registry.get_registered_types()
longer_prefixes = [
pt.upper() + "_"
for pt in plugin_types
if pt != plugin_type and pt.upper().startswith(plugin_type.upper() + "_")
]
# Find all project IDs for this plugin type # Find all project IDs for this plugin type
project_ids = set() project_ids = set()
env_pattern = re.compile(f"^{prefix}([A-Z0-9_]+?)_(.+)$") env_pattern = re.compile(f"^{prefix}([A-Z0-9_]+?)_(.+)$")
for env_key in os.environ.keys(): for env_key in os.environ.keys():
# Skip env vars that belong to a more specific plugin type
if any(env_key.startswith(lp) for lp in longer_prefixes):
continue
match = env_pattern.match(env_key) match = env_pattern.match(env_key)
if match: if match:
project_id = match.group(1).lower() project_id = match.group(1).lower()
@@ -78,9 +91,7 @@ class ProjectManager:
if config: if config:
self._create_project_instance(plugin_type, project_id, config) self._create_project_instance(plugin_type, project_id, config)
except Exception as e: except Exception as e:
self.logger.error( self.logger.debug(f"Legacy ProjectManager: skipped {plugin_type}/{project_id}: {e}")
f"Failed to create {plugin_type} project '{project_id}': {e}", exc_info=True
)
def _load_project_config(self, plugin_type: str, project_id: str) -> dict[str, Any] | None: def _load_project_config(self, plugin_type: str, project_id: str) -> dict[str, Any] | None:
""" """

View File

@@ -211,12 +211,27 @@ class SiteManager:
""" """
prefix = plugin_type.upper() + "_" prefix = plugin_type.upper() + "_"
# Build list of longer prefixes from other plugin types to avoid collisions.
# e.g. WORDPRESS_ must not match WORDPRESS_ADVANCED_ env vars.
from plugins import registry as plugin_registry
all_plugin_types = plugin_registry.get_registered_types()
longer_prefixes = [
pt.upper() + "_"
for pt in all_plugin_types
if pt != plugin_type and pt.upper().startswith(plugin_type.upper() + "_")
]
# Pattern to match: WORDPRESS_SITE1_URL, WORDPRESS_SITE2_USERNAME, etc. # Pattern to match: WORDPRESS_SITE1_URL, WORDPRESS_SITE2_USERNAME, etc.
env_pattern = re.compile(f"^{prefix}([A-Z0-9_]+?)_(.+)$") env_pattern = re.compile(f"^{prefix}([A-Z0-9_]+?)_(.+)$")
# Find all unique site IDs # Find all unique site IDs
site_ids = set() site_ids = set()
for env_key in os.environ.keys(): for env_key in os.environ.keys():
# Skip env vars that belong to a more specific plugin type
if any(env_key.startswith(lp) for lp in longer_prefixes):
continue
match = env_pattern.match(env_key) match = env_pattern.match(env_key)
if match: if match:
site_id = match.group(1).lower() site_id = match.group(1).lower()
@@ -486,22 +501,15 @@ 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:
return config.alias
return full_id
plugin_type, site_id = parts return full_id
# 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 full_id
except ValueError:
return full_id
def get_alias_conflicts(self) -> dict[str, list[str]]: def get_alias_conflicts(self) -> dict[str, list[str]]:
""" """

View File

@@ -94,15 +94,11 @@ class SiteRegistry:
# Log alias conflicts if any # Log alias conflicts if any
if self.alias_conflicts: if self.alias_conflicts:
self.logger.warning("=" * 50) self.logger.info("Duplicate alias conflicts detected:")
self.logger.warning("DUPLICATE ALIAS CONFLICTS DETECTED:")
for alias, full_ids in self.alias_conflicts.items(): for alias, full_ids in self.alias_conflicts.items():
winner = self.aliases.get(alias) winner = self.aliases.get(alias)
losers = [fid for fid in full_ids if fid != winner] 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)")
f" Alias '{alias}': {winner} (winner), {losers} (using full_id)"
)
self.logger.warning("=" * 50)
# Reserved words that should NOT be interpreted as site IDs # Reserved words that should NOT be interpreted as site IDs
RESERVED_SITE_WORDS = { RESERVED_SITE_WORDS = {
@@ -206,7 +202,7 @@ class SiteRegistry:
if alias not in self.alias_conflicts: if alias not in self.alias_conflicts:
self.alias_conflicts[alias] = [existing_full_id] self.alias_conflicts[alias] = [existing_full_id]
self.alias_conflicts[alias].append(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"Duplicate alias '{alias}': {full_id} conflicts with {existing_full_id}. "
f"{full_id} will use full_id for endpoint path." f"{full_id} will use full_id for endpoint path."
) )

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}MCP Hub{% endblock %}</title> <title>{% block title %}MCP Hub{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="/static/logo.svg">
<!-- Tailwind CSS from CDN --> <!-- Tailwind CSS from CDN -->
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ t.dashboard }}{% endblock %} - MCP Hub</title> <title>{% block title %}{{ t.dashboard }}{% endblock %} - MCP Hub</title>
<link rel="icon" type="image/svg+xml" href="/static/logo.svg">
<!-- Vazirmatn Font for Persian --> <!-- Vazirmatn Font for Persian -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -119,13 +120,12 @@
<!-- Logo --> <!-- Logo -->
<div class="flex items-center justify-between h-16 px-4 border-b border-gray-700"> <div class="flex items-center justify-between h-16 px-4 border-b border-gray-700">
<div class="flex items-center" x-show="sidebarOpen"> <div class="flex items-center" x-show="sidebarOpen">
<div class="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center"> <img src="/static/logo.svg" alt="MCP Hub" class="w-8 h-8">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<span class="{% if lang == 'fa' %}mr-3{% else %}ml-3{% endif %} font-bold text-lg">MCP Hub</span> <span class="{% if lang == 'fa' %}mr-3{% else %}ml-3{% endif %} font-bold text-lg">MCP Hub</span>
</div> </div>
<div x-show="!sidebarOpen" class="flex items-center justify-center">
<img src="/static/logo.svg" alt="MCP Hub" class="w-8 h-8">
</div>
<button <button
@click="sidebarOpen = !sidebarOpen" @click="sidebarOpen = !sidebarOpen"
class="p-2 rounded-lg hover:bg-gray-700 transition-colors" class="p-2 rounded-lg hover:bg-gray-700 transition-colors"

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ t.login_title }} - MCP Hub</title> <title>{{ t.login_title }} - MCP Hub</title>
<link rel="icon" type="image/svg+xml" href="/static/logo.svg">
<!-- Tailwind CSS --> <!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -108,13 +109,20 @@
<svg class="w-5 h-5 text-blue-400 {% if lang == 'fa' %}ml-2{% else %}mr-2{% endif %} mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-blue-400 {% if lang == 'fa' %}ml-2{% else %}mr-2{% endif %} mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
<p class="text-sm text-blue-300"> <div class="text-sm text-blue-300">
{% if lang == 'fa' %} {% if lang == 'fa' %}
برای دسترسی به داشبورد به Master API Key یا یک API Key با scope=admin نیاز دارید. <p>برای دسترسی به داشبورد به Master API Key نیاز دارید.</p>
<p class="mt-2 text-blue-400">کلید API خود را در متغیر محیطی <code class="bg-gray-700 px-1 rounded">MASTER_API_KEY</code> تنظیم کنید.</p>
{% else %} {% else %}
You need a Master API Key or an API Key with scope=admin to access the dashboard. <p>Enter your <code class="bg-gray-700 px-1 rounded">MASTER_API_KEY</code> to access the dashboard.</p>
<p class="mt-2 text-blue-400">Don't have one? Set it in your <code class="bg-gray-700 px-1 rounded">.env</code> file, or check the server logs for the temporary key.</p>
{% endif %} {% endif %}
</p> <p class="mt-2">
<a href="https://github.com/airano-ir/mcphub#quick-start" target="_blank" class="text-purple-400 hover:text-purple-300 underline">
{% if lang == 'fa' %}راهنمای شروع{% else %}Setup Guide{% endif %}
</a>
</p>
</div>
</div> </div>
</div> </div>
@@ -131,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 v3.0.0 MCP Hub v{{ version|default('3.0.1') }}
</p> </p>
</div> </div>
</body> </body>

View File

@@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024"><style>.a{fill:#51b9f4}.b{fill:#fec13d}</style><path class="a" d="m496.4 11.5q5.4-0.4 10.9-0.5 5.4-0.1 10.9 0.1 5.4 0.2 10.8 0.7 5.5 0.4 10.9 1.2c56.2 7.7 99.4 36 133.3 81.3 31 41.4 41 94.3 34.8 144.5-6.9 54.7-31.6 89.2-71.8 124.8-9.7 8.6-19.4 21-29.9 28.7 8 7.2 17.5 17 25.1 24.8 8.6-11.9 22.8-24.8 33.4-35.4 10.6-10.1 20.7-22.2 32.2-31.2 44.5-34.8 109.1-43.7 163.2-31.3 97.6 22.4 162.8 115.9 151.4 215.3-12.8 111.6-114.9 189.3-225.7 173.7-56.9-8-89.1-29.9-126.7-72.5-8.4-9.5-20.3-19-28.1-29.2-3 4.4-20.1 21-24.7 25.2q0.8 0.7 1.6 1.4c10.3 8.9 20.5 20.3 30 29.2 31.4 29.2 55.6 55.7 65.1 98.8 3 13.7 4.9 22.1 6.1 36.6 4.1 52.5-11.2 105-46 144.9-28.1 32.3-64.1 56.5-106.4 64.9-9.6 2-17.3 3.9-27.3 4.6-11.3 1.4-27.3 0.9-38.7-0.3-55.1-6.2-100.6-31.3-135.2-74.7-6.8-8.5-12.8-17.6-18.1-27.2-5.2-9.5-9.7-19.4-13.3-29.7-3.6-10.3-6.3-20.8-8.1-31.6-1.9-10.7-2.8-21.5-2.8-32.4-0.3-41.9 11.1-88.4 38.8-121 5-5.8 10.5-11.8 16.2-17.1 15.8-14.8 31.2-33.3 48-46.7-7.7-8-17.1-16.5-24.2-24.6-8.4 9.8-18.5 19-27.4 28.3-10.5 10.8-20.7 21.7-31.8 31.9-36.3 32.8-85.8 44.5-133.6 42.7-12.8-0.5-25.5-2.3-38-5.3-12.5-3-24.6-7.3-36.2-12.7-11.7-5.4-22.7-11.9-33.1-19.4-10.3-7.6-19.9-16.2-28.6-25.6-46.8-50.7-62.6-116.6-46.9-183.1 8.7-41.1 30.3-72.1 60.7-100.4 64.9-60.6 180.9-67.1 250.4-10.7 10.9 8.8 22.5 21.5 32.4 31.7 10.5 10.9 21.9 21.7 32.1 33 8.1-11 14.4-15.9 23.9-24.9-37.7-41.9-77.8-63.2-94-122.1-17.9-65.2-8.7-131.9 34.5-185.2 37.9-46.8 81.1-66.8 139.9-73.5zm-44.5 380.8q-5.1 5.1-10.1 10.1-5.1 5-10.2 10.1-5 5-10.1 10.1-5 5-10 10.1c-3.7 3.7-16.3 16.8-20.1 19.1-12.9-14.3-28.9-27.9-41.3-41.9-35.7-40-68.4-67.4-124.3-70.7-53.9-3.2-94.4 9.6-135 46-8.2 7.5-15.6 15.7-22.2 24.7-6.6 8.9-12.3 18.5-16.9 28.5-4.7 10.1-8.4 20.6-11.1 31.4-2.6 10.7-4.2 21.7-4.6 32.8-6.6 133.7 129.3 221.3 249 166.3 3.7-2.1 7.3-4.3 10.8-6.5 10.9-6.8 20.8-15.5 30.3-24.1 8.2-7.5 14.8-16.4 22.4-24.5 13.8-14.7 29-28.3 43.3-42.4 4.5 5.4 11.9 12.5 17.1 17.6l28.4 28.4c3.5 3.5 11.1 10.7 14.1 14.3-7.4 9.1-18.4 18.4-26.7 27.1-18.8 19.6-40.3 36.2-56.9 57.9-24.9 32.5-31.7 75.7-28.5 115.7 4 47.7 27.3 87 63.8 117.4 6.4 5.3 13.3 10.2 20.5 14.5 7.2 4.3 14.8 8 22.5 11.2 7.8 3.2 15.8 5.8 23.9 7.8 8.2 2 16.5 3.4 24.8 4.2 7.8 0.6 17.8 0.5 25.6 0.3 35.2-1.1 68.9-14.1 95.9-36.2 8.1-6.7 12.7-9.6 20.1-17.7 33.7-36.9 49.6-74.3 48.1-124.8-1.2-38-9.3-69-34.2-98.8-10.3-12.4-23-23.8-34.6-34.8-14.7-14-29.1-29.5-43.8-43.4l-0.3-0.3c5.7-7.1 11.7-13.2 18.2-19.6 13.7-13.5 26.9-27.5 41.1-40.5l0.6 0.6c9.5 7.9 18.7 19.1 27.8 27.7 18.6 17.9 34.5 38.1 54.9 54.1 33.2 26.3 76.5 33.7 117.8 30.3 52.5-4.3 98.3-36.2 127.8-78.8 18.8-27.3 28.5-62 27.5-95.2-0.2-5 0-9.8-0.4-14.8-1-11.4-3.1-22.6-6.3-33.5-3.2-10.9-7.6-21.5-12.9-31.6-5.4-10-11.7-19.5-19-28.3-7.2-8.8-15.4-16.8-24.2-24-40.2-32.9-81.7-43.3-132.6-38.3-50.9 4.9-82 28.5-114.5 65.8-6.4 7.3-15 14.7-21.6 22-5.5 5.4-19.7 20.8-25 24.3-19.2-20.2-40.1-39.5-59.3-59.7 12.5-15 27.9-29.5 41.9-43.3 5.9-5.8 13.8-11.6 20.3-18.1 32.1-32.4 48.9-63.7 50.5-110 0.4-10.8 0.6-19.1-0.4-30.1-1.1-11.2-3.3-22.3-6.6-33.1-3.3-10.7-7.6-21.2-13-31.1-5.3-9.9-11.7-19.3-18.9-27.9-7.2-8.7-15.3-16.6-24-23.7-29.7-24.3-61.1-36.9-99.7-39-4.5-0.2-14.1-0.4-18.5 0.1-49.6 3.5-86.3 20.5-120.3 57.4-33.6 36.6-46.8 80.9-43.8 130 2.7 45.3 18.5 77.4 51.2 108.8 20 19.3 42.8 39.8 61.7 60z"/><path class="b" d="m210.1 432.1c43.8 0.6 78.9 36.4 78.7 80.1-0.3 43.8-35.9 79.2-79.6 79.2-44.3 0-80-35.9-79.8-80.1 0.3-44.2 36.5-79.8 80.7-79.2zm1.1 134.1c30-1.2 53.3-26.3 52.3-56.3-1-29.9-26-53.4-55.9-52.6-30.3 0.8-54.1 26-53.1 56.3 1 30.2 26.5 53.9 56.7 52.6z"/><path class="b" d="m503.9 129.7c43.8-4.5 82.9 27.4 87.3 71.2 4.4 43.8-27.5 82.9-71.3 87.3-43.8 4.3-82.8-27.6-87.2-71.3-4.4-43.8 27.5-82.8 71.2-87.2zm12.9 133.5c29.9-2.7 52-29.1 49.4-59.1-2.7-30-29.1-52.1-59.1-49.5-30 2.6-52.3 29.1-49.6 59.1 2.7 30.1 29.2 52.3 59.3 49.5z"/><path class="b" d="m503.3 735.5c43.8-4.8 83.1 26.9 87.9 70.7 4.7 43.7-27 83-70.8 87.7-43.7 4.6-82.9-27-87.6-70.7-4.7-43.7 26.9-82.9 70.5-87.7zm12.5 133.5c29.9-2.1 52.5-28 50.5-58-2-30-27.8-52.7-57.8-50.8-30.2 1.8-53.1 27.9-51.1 58 2 30.2 28.2 53 58.4 50.8z"/><path class="b" d="m805.8 432.7c43.7-5 83.3 26.4 88.2 70.2 4.9 43.7-26.6 83.2-70.3 88-43.7 4.9-83-26.5-87.9-70.2-5-43.6 26.4-83 70-88zm11.6 133.6c29.9-1.5 53-26.9 51.8-56.8-1.3-30-26.5-53.3-56.4-52.2-30.3 1-53.9 26.5-52.7 56.8 1.3 30.3 27 53.7 57.3 52.2z"/><path class="a" d="m511 451.9c3.1 0.9 33.1 32.8 37.8 36.9 2.7 2.4 19.9 19.4 22.3 22.7-2.6 4.7-53.2 55.7-59.3 60.2-0.2 0-0.4 0-0.5-0.1-2.1-0.8-56.2-54.4-59.3-59.3 1.4-4.2 52.5-54.7 59-60.4zm-24.9 59.5c3.3 4.6 19.8 20.5 24.8 25.4 0.5 0 1 0.1 1.4 0 8-8.8 16-16.6 24.6-24.8-3-3-24.1-24.6-25.8-25.3-3.7 4.3-21 21.9-25 24.7z"/></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -2,16 +2,17 @@
# MCP Hub — Docker Compose Configuration # MCP Hub — Docker Compose Configuration
# =================================== # ===================================
# #
# Build Pack: Docker Compose # After starting:
# ⚠️ CRITICAL RULES FOR COOLIFY: # docker compose up -d
# 1. NO host port mappings: Use "8000" NOT "8000:8000" # curl http://localhost:8000/health # verify server is running
# 2. Listen on 0.0.0.0 (NOT localhost) # open http://localhost:8000/dashboard # web dashboard
# 3. Health checks for ALL services #
# 4. Use environment variables for ALL configs # Port mapping:
# - Standalone Docker: ports "8000:8000" works out-of-the-box.
# - Coolify: Comment out or remove the 'ports' section below.
# Coolify's reverse proxy handles routing — 'expose' is sufficient.
# =================================== # ===================================
version: '3.8'
services: services:
mcp-server: mcp-server:
build: build:
@@ -20,10 +21,11 @@ services:
container_name: mcphub container_name: mcphub
restart: unless-stopped restart: unless-stopped
# ⚠️ CRITICAL: Only container port (NO host port mapping)
# Coolify will handle routing via domain
ports: ports:
- "8000" - "8000:8000"
# Coolify users: Remove the 'ports' section above.
# Coolify's reverse proxy routes traffic to the container's exposed port.
# Environment variables # Environment variables
environment: environment:
@@ -34,22 +36,17 @@ services:
- LOG_LEVEL=${LOG_LEVEL:-INFO} - LOG_LEVEL=${LOG_LEVEL:-INFO}
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
# === OAuth 2.1 Configuration === # === OAuth 2.1 (OPTIONAL) ===
# Required for OAuth authentication # Only needed for ChatGPT auto-registration or third-party OAuth clients.
- OAUTH_JWT_SECRET_KEY=${OAUTH_JWT_SECRET_KEY} # For Claude Desktop/Code/Cursor with Bearer token auth, skip these entirely.
- OAUTH_JWT_ALGORITHM=${OAUTH_JWT_ALGORITHM:-HS256} # - OAUTH_JWT_SECRET_KEY=${OAUTH_JWT_SECRET_KEY}
- OAUTH_ACCESS_TOKEN_TTL=${OAUTH_ACCESS_TOKEN_TTL:-3600} # - OAUTH_BASE_URL=${OAUTH_BASE_URL}
- OAUTH_REFRESH_TOKEN_TTL=${OAUTH_REFRESH_TOKEN_TTL:-604800} # - OAUTH_JWT_ALGORITHM=${OAUTH_JWT_ALGORITHM:-HS256}
# - OAUTH_ACCESS_TOKEN_TTL=${OAUTH_ACCESS_TOKEN_TTL:-3600}
# - OAUTH_REFRESH_TOKEN_TTL=${OAUTH_REFRESH_TOKEN_TTL:-604800}
- OAUTH_STORAGE_TYPE=${OAUTH_STORAGE_TYPE:-json} - OAUTH_STORAGE_TYPE=${OAUTH_STORAGE_TYPE:-json}
- OAUTH_STORAGE_PATH=${OAUTH_STORAGE_PATH:-/app/data} - OAUTH_STORAGE_PATH=${OAUTH_STORAGE_PATH:-/app/data}
# OAuth Authorization Security
# IMPORTANT: API Key is always required for authorization (OAuth manual mode)
# Use 'required' (recommended) to enforce API Key authentication
- OAUTH_AUTH_MODE=${OAUTH_AUTH_MODE:-required}
# OAUTH_TRUSTED_DOMAINS is deprecated - API Key always required for security
- OAUTH_BASE_URL=${OAUTH_BASE_URL}
# === WordPress Projects === # === WordPress Projects ===
# Configure WordPress sites in Coolify environment variables # Configure WordPress sites in Coolify environment variables
# Format: WORDPRESS_{SITE_ID}_{CONFIG_KEY} # Format: WORDPRESS_{SITE_ID}_{CONFIG_KEY}

View File

@@ -48,14 +48,18 @@ For each WordPress site you want to manage:
git clone https://github.com/airano-ir/mcphub.git git clone https://github.com/airano-ir/mcphub.git
cd mcphub cd mcphub
cp env.example .env cp env.example .env
# Edit .env with your site credentials # Edit .env — set MASTER_API_KEY and add your site credentials (see Configuration below)
docker compose up -d docker compose up -d
``` ```
### Option 2: PyPI After starting, see [Verify Installation](#verify-installation) below.
### Option 2: Docker Hub (No Clone)
```bash ```bash
pip install mcphub-server # 1. Create a .env file (see Configuration section below)
# 2. Run:
docker run -d --name mcphub -p 8000:8000 --env-file .env airano/mcphub:latest
``` ```
### Option 3: From Source ### Option 3: From Source
@@ -66,7 +70,7 @@ cd mcphub
pip install -e . pip install -e .
cp env.example .env cp env.example .env
# Edit .env with your site credentials # Edit .env with your site credentials
python server.py --transport sse --port 8000 python server.py --transport streamable-http --port 8000
``` ```
### Option 4: Automated Setup Scripts ### Option 4: Automated Setup Scripts
@@ -125,7 +129,7 @@ Edit the `.env` file with your credentials:
```bash ```bash
# ============================================ # ============================================
# Required # Authentication (recommended — auto-generates temp key if omitted)
# ============================================ # ============================================
MASTER_API_KEY=your-secure-key-here MASTER_API_KEY=your-secure-key-here
@@ -167,6 +171,126 @@ RATE_LIMIT_PER_HOUR=1000
RATE_LIMIT_PER_DAY=10000 RATE_LIMIT_PER_DAY=10000
``` ```
### WordPress Plugin Requirements
Some tools require specific WordPress plugins or infrastructure:
| 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 / WordPress Advanced
WP-CLI and WordPress Advanced system tools require Docker socket access:
```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
```
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
### Environment Variable Reference
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`)
- 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 ### Configuration Tips
- **Site Aliases**: Use friendly names like `myblog`, `mystore`, or `mygitea` - **Site Aliases**: Use friendly names like `myblog`, `mystore`, or `mygitea`
@@ -178,10 +302,10 @@ RATE_LIMIT_PER_DAY=10000
## Running the Server ## Running the Server
### SSE Transport (for remote AI clients) ### Streamable HTTP Transport (for remote AI clients)
```bash ```bash
python server.py --transport sse --port 8000 python server.py --transport streamable-http --port 8000
``` ```
### Stdio Transport (for Claude Desktop local) ### Stdio Transport (for Claude Desktop local)
@@ -190,26 +314,60 @@ python server.py --transport sse --port 8000
python server.py python server.py
``` ```
### Verify Server is Running ### Verify Installation
Check logs for: After starting (via Docker or locally), wait ~30 seconds for the server to initialize, then:
``` **1. Check health:**
INFO: MCP Hub initialized
INFO: Registered 589 tools
INFO: Server ready
```
Or test the health endpoint:
```bash ```bash
curl http://localhost:8000/health curl http://localhost:8000/health
# Expected: {"status": "ok", "tools_loaded": 596, ...}
``` ```
**2. Open the web dashboard:**
Open **http://localhost:8000/dashboard** in your browser. Log in with your `MASTER_API_KEY`.
The dashboard lets you:
- View all connected sites and their health status
- Create and manage per-project API keys
- View audit logs
- Monitor rate limits
**3. Check container status (Docker only):**
```bash
docker compose ps
# Look for Status: "Up (healthy)"
# Note: Health check starts after 40 seconds — "starting" is normal initially
# View logs if something is wrong:
docker compose logs -f mcphub
```
**4. Troubleshooting:**
| Problem | Solution |
|---------|----------|
| Container exits immediately | Check logs: `docker compose logs mcphub` |
| Port 8000 already in use | Change port in docker-compose.yaml: `"8001:8000"` |
| Health check shows "unhealthy" | Wait 60 seconds, then check logs for startup errors |
| Dashboard login fails | Make sure you're using the `MASTER_API_KEY` value from your `.env` |
| Sites not showing up | Restart after adding new env vars: `docker compose restart` |
--- ---
## Connect Your AI Client ## Connect Your AI Client
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 ### Claude Desktop
Add to `claude_desktop_config.json`: Add to `claude_desktop_config.json`:
@@ -217,8 +375,9 @@ Add to `claude_desktop_config.json`:
```json ```json
{ {
"mcpServers": { "mcpServers": {
"mcphub": { "mcphub-wordpress": {
"url": "https://your-server:8000/mcp", "type": "streamableHttp",
"url": "http://your-server:8000/wordpress/mcp",
"headers": { "headers": {
"Authorization": "Bearer YOUR_MASTER_API_KEY" "Authorization": "Bearer YOUR_MASTER_API_KEY"
} }
@@ -234,9 +393,9 @@ Add to `.mcp.json` in your project:
```json ```json
{ {
"mcpServers": { "mcpServers": {
"mcphub": { "mcphub-wordpress": {
"type": "sse", "type": "http",
"url": "https://your-server:8000/mcp", "url": "http://your-server:8000/wordpress/mcp",
"headers": { "headers": {
"Authorization": "Bearer YOUR_MASTER_API_KEY" "Authorization": "Bearer YOUR_MASTER_API_KEY"
} }
@@ -249,8 +408,8 @@ Add to `.mcp.json` in your project:
Go to **Settings > MCP Servers > Add Server**: Go to **Settings > MCP Servers > Add Server**:
- **Name**: MCP Hub - **Name**: MCP Hub WordPress
- **URL**: `https://your-server:8000/mcp` - **URL**: `http://your-server:8000/wordpress/mcp`
- **Headers**: `Authorization: Bearer YOUR_MASTER_API_KEY` - **Headers**: `Authorization: Bearer YOUR_MASTER_API_KEY`
### VS Code + Copilot ### VS Code + Copilot
@@ -260,9 +419,9 @@ Add to `.vscode/mcp.json`:
```json ```json
{ {
"servers": { "servers": {
"mcphub": { "mcphub-wordpress": {
"type": "sse", "type": "http",
"url": "https://your-server:8000/mcp", "url": "http://your-server:8000/wordpress/mcp",
"headers": { "headers": {
"Authorization": "Bearer YOUR_MASTER_API_KEY" "Authorization": "Bearer YOUR_MASTER_API_KEY"
} }
@@ -279,24 +438,26 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
2. In ChatGPT, add MCP server: `https://your-server:8000/mcp` 2. In ChatGPT, add MCP server: `https://your-server:8000/mcp`
3. ChatGPT auto-discovers OAuth metadata and registers 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 ## Using MCP Tools
### 589 Tools Across 9 Plugins ### 596 Tools Across 9 Plugins
| Plugin | Tools | Env Prefix | | Plugin | Tools | Env Prefix |
|--------|-------|------------| |--------|-------|------------|
| WordPress | 67 | `WORDPRESS_` | | WordPress | 67 | `WORDPRESS_` |
| WooCommerce | 28 | `WOOCOMMERCE_` | | WooCommerce | 28 | `WOOCOMMERCE_` |
| WordPress Advanced | 22 | `WORDPRESS_` (same sites, advanced ops) | | WordPress Advanced | 22 | `WORDPRESS_ADVANCED_` |
| Gitea | 56 | `GITEA_` | | Gitea | 56 | `GITEA_` |
| n8n | 56 | `N8N_` | | n8n | 56 | `N8N_` |
| Supabase | 70 | `SUPABASE_` | | Supabase | 70 | `SUPABASE_` |
| OpenPanel | 73 | `OPENPANEL_` | | OpenPanel | 73 | `OPENPANEL_` |
| Appwrite | 100 | `APPWRITE_` | | Appwrite | 100 | `APPWRITE_` |
| Directus | 100 | `DIRECTUS_` | | Directus | 100 | `DIRECTUS_` |
| System | 17 | (no config needed) | | System | 24 | (no config needed) |
### Unified Tool Pattern ### Unified Tool Pattern
@@ -313,16 +474,40 @@ The `site` parameter accepts either a **site_id** (e.g., `site1`) or an **alias*
### Multi-Endpoint Architecture ### Multi-Endpoint Architecture
Use specific endpoints to limit tool access: Use the most specific endpoint for your use case to minimize token usage:
``` | Endpoint | Use Case | Tools | `site` param? |
/mcp → All 589 tools (Master API Key) |----------|----------|------:|:-------------:|
/system/mcp → System tools only (17 tools) | `/project/{alias}/mcp` | Single-site workflow | 22-100 | No (pre-scoped) |
/wordpress/mcp → WordPress tools (67 tools) | `/{plugin}/mcp` | Multi-site management | 23-101 | Yes |
/woocommerce/mcp → WooCommerce tools (28 tools) | `/system/mcp` | System administration | 24 | N/A |
/gitea/mcp → Gitea tools (56 tools) | `/mcp` | Admin & discovery only | 596 | Yes |
/project/{alias}/mcp → Per-project (auto-injects site)
``` > **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 (`/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) |
--- ---
@@ -334,34 +519,40 @@ Use specific endpoints to limit tool access:
docker compose up -d docker compose up -d
``` ```
After starting, verify the installation:
```bash
curl http://localhost:8000/health # server health
open http://localhost:8000/dashboard # web dashboard
```
See [Verify Installation](#verify-installation) for detailed steps.
### Docker Commands ### Docker Commands
```bash ```bash
# View logs # View logs
docker compose logs -f docker compose logs -f mcphub
# Check status # Check status (look for "healthy")
docker compose ps docker compose ps
# Restart # Restart (needed after .env changes)
docker compose restart docker compose restart
# Stop # Stop
docker compose down docker compose down
# Rebuild # Rebuild (after code changes)
docker compose up --build -d docker compose up --build -d
``` ```
### Health Check ### Adding Sites After Startup
```bash 1. Edit your `.env` file to add new site credentials
# Check container health 2. Restart the container: `docker compose restart`
docker compose ps 3. Verify: `curl http://localhost:8000/health` — check that tools are loaded
4. The dashboard at http://localhost:8000/dashboard will show the new sites
# Test API endpoint
curl http://localhost:8000/health
```
--- ---
@@ -412,7 +603,7 @@ The server auto-discovers all `WORDPRESS_*`, `WOOCOMMERCE_*`, `GITEA_*`, and oth
## Next Steps ## Next Steps
1. **Explore the full tool list**: See the [README](../README.md) for all 589 tools 1. **Explore the full tool list**: See the [README](../README.md) for all 596 tools
2. **Set up API keys**: [API Keys Guide](API_KEYS_GUIDE.md) for per-project access control 2. **Set up API keys**: [API Keys Guide](API_KEYS_GUIDE.md) for per-project access control
3. **Configure OAuth**: [OAuth Guide](OAUTH_GUIDE.md) for Claude/ChatGPT auto-registration 3. **Configure OAuth**: [OAuth Guide](OAUTH_GUIDE.md) for Claude/ChatGPT auto-registration
4. **Monitor health**: Use `check_all_projects_health` tool or visit the web dashboard 4. **Monitor health**: Use `check_all_projects_health` tool or visit the web dashboard

135
env.example Normal file
View File

@@ -0,0 +1,135 @@
# ===================================
# MCP Hub — Environment Configuration
# ===================================
#
# MINIMUM TO START: Just set MASTER_API_KEY and one site below.
# Full docs: https://github.com/airano-ir/mcphub/blob/main/docs/getting-started.md
#
# After editing, run:
# docker compose up -d
# curl http://localhost:8000/health # verify server
# open http://localhost:8000/dashboard # web dashboard
# ===================================
# ============================================
# REQUIRED
# ============================================
# Master API key for authentication (also used to log into the dashboard)
# Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))"
MASTER_API_KEY=your-secure-key-here
# ============================================
# WORDPRESS SITES
# ============================================
# Pattern: WORDPRESS_{SITE_ID}_{CONFIG_KEY}
# Add as many sites as you want (SITE1, SITE2, SITE3, ...)
#
# How to create a WordPress Application Password:
# 1. Log into WordPress admin → Users → Your Profile
# 2. Scroll to "Application Passwords" section
# 3. Enter name "MCP Hub" → click "Add New"
# 4. Copy the generated password (format: xxxx xxxx xxxx xxxx)
WORDPRESS_SITE1_URL=https://your-site.com
WORDPRESS_SITE1_USERNAME=admin
WORDPRESS_SITE1_APP_PASSWORD=xxxx xxxx xxxx xxxx
WORDPRESS_SITE1_ALIAS=mysite
# Optional: Docker container name for WP-CLI tools (cache flush, transients, etc.)
# Find your container name with: docker ps --filter name=wordpress
# This lets MCP Hub run wp-cli commands inside your WordPress container.
# WORDPRESS_SITE1_CONTAINER=wordpress-container-name
# Second site example (uncomment to use):
# WORDPRESS_SITE2_URL=https://another-site.com
# WORDPRESS_SITE2_USERNAME=admin
# WORDPRESS_SITE2_APP_PASSWORD=yyyy yyyy yyyy yyyy
# WORDPRESS_SITE2_ALIAS=blog
# ============================================
# WORDPRESS ADVANCED (database, bulk, system)
# ============================================
# Pattern: WORDPRESS_ADVANCED_{SITE_ID}_{CONFIG_KEY}
# Provides database operations, bulk operations, and system management.
# REQUIRES Docker socket AND container name — all tools use WP-CLI.
#
# Uses the same credentials as WordPress, but the CONTAINER key is REQUIRED.
# 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-advanced
# ============================================
# WOOCOMMERCE STORES
# ============================================
# Pattern: WOOCOMMERCE_{STORE_ID}_{CONFIG_KEY}
# WooCommerce is a separate plugin — uses Consumer Key/Secret, not App Passwords.
#
# Create keys: WooCommerce → Settings → Advanced → REST API → Add Key
# WOOCOMMERCE_STORE1_URL=https://your-store.com
# WOOCOMMERCE_STORE1_CONSUMER_KEY=ck_xxxxx
# WOOCOMMERCE_STORE1_CONSUMER_SECRET=cs_xxxxx
# WOOCOMMERCE_STORE1_ALIAS=mystore
# ============================================
# OTHER PLUGINS (uncomment to use)
# ============================================
# All plugins auto-discover their environment variables at startup.
# Pattern: {PLUGIN_TYPE}_{INSTANCE_ID}_{CONFIG_KEY}
# --- Gitea ---
# GITEA_REPO1_URL=https://git.example.com
# GITEA_REPO1_TOKEN=your_gitea_token
# GITEA_REPO1_ALIAS=mygitea
# --- n8n ---
# N8N_INSTANCE1_URL=https://n8n.example.com
# N8N_INSTANCE1_API_KEY=your_n8n_api_key
# N8N_INSTANCE1_ALIAS=myn8n
# --- Supabase ---
# SUPABASE_PROJECT1_URL=https://xxxxx.supabase.co
# SUPABASE_PROJECT1_API_KEY=your_supabase_api_key
# SUPABASE_PROJECT1_SERVICE_ROLE=your_service_role_key
# SUPABASE_PROJECT1_ALIAS=mysupabase
# --- OpenPanel ---
# OPENPANEL_INSTANCE1_URL=https://openpanel.example.com
# OPENPANEL_INSTANCE1_CLIENT_ID=your_client_id
# OPENPANEL_INSTANCE1_CLIENT_SECRET=your_client_secret
# OPENPANEL_INSTANCE1_ALIAS=myopenpanel
# --- Appwrite ---
# APPWRITE_PROJECT1_URL=https://appwrite.example.com
# APPWRITE_PROJECT1_API_KEY=your_appwrite_api_key
# APPWRITE_PROJECT1_PROJECT_ID=your_project_id
# APPWRITE_PROJECT1_ALIAS=myappwrite
# --- Directus ---
# DIRECTUS_INSTANCE1_URL=https://directus.example.com
# DIRECTUS_INSTANCE1_TOKEN=your_directus_token
# DIRECTUS_INSTANCE1_ALIAS=mydirectus
# ============================================
# OAUTH (optional — for ChatGPT/Claude auto-registration)
# ============================================
# Only needed if you want ChatGPT or Claude to auto-register via OAuth.
# For local testing with API key auth, you can skip these.
# OAUTH_JWT_SECRET_KEY=your-jwt-secret
# OAUTH_BASE_URL=https://your-public-domain.com
# ============================================
# ADVANCED (optional — defaults are fine)
# ============================================
# Logging
LOG_LEVEL=INFO
# Rate limits (per client)
# RATE_LIMIT_PER_MINUTE=60
# RATE_LIMIT_PER_HOUR=1000
# RATE_LIMIT_PER_DAY=10000

View File

@@ -10,9 +10,6 @@ Security:
- WP-CLI installation check - WP-CLI installation check
- Timeout protection (30s default) - Timeout protection (30s default)
- Graceful error handling - Graceful error handling
Phase 5.1: Cache Management (4 tools)
Phase 5.2: Database & Plugin/Theme Info (7 tools)
""" """
import asyncio import asyncio
@@ -75,10 +72,7 @@ class WPCLIManager:
Check if the Docker container exists and is running. Check if the Docker container exists and is running.
Returns: Returns:
bool: True if container exists and is running bool: True if container exists and is running, False otherwise
Raises:
Exception: If docker command fails or container not found
""" """
try: try:
# First, test if we have Docker socket access # First, test if we have Docker socket access
@@ -93,15 +87,12 @@ class WPCLIManager:
if test_process.returncode != 0: if test_process.returncode != 0:
error_msg = test_stderr.decode().strip() error_msg = test_stderr.decode().strip()
self.logger.error(f"Cannot access Docker daemon: {error_msg}") self.logger.warning(
raise Exception( f"Docker daemon not accessible for container '{self.container_name}': "
f"Cannot access Docker daemon. This is likely a permissions issue. " f"{error_msg}. WP-CLI features will be unavailable. "
f"Error: {error_msg}. " f"Mount /var/run/docker.sock to enable WP-CLI."
f"Please ensure:\n"
f"1. Docker socket is mounted: /var/run/docker.sock\n"
f"2. User has permission to access Docker (member of docker group)\n"
f"3. Docker daemon is running on the host"
) )
return False
docker_version = test_stdout.decode().strip() docker_version = test_stdout.decode().strip()
self.logger.debug(f"Docker access OK - Server version: {docker_version}") self.logger.debug(f"Docker access OK - Server version: {docker_version}")
@@ -151,15 +142,14 @@ class WPCLIManager:
return True return True
except TimeoutError: except TimeoutError:
self.logger.error("Docker command timed out") self.logger.warning(
raise Exception("Docker command timed out after 5 seconds") f"Docker command timed out for container '{self.container_name}'. "
f"WP-CLI features will be unavailable."
)
return False
except Exception as e: except Exception as e:
# Re-raise exceptions with full context self.logger.warning(f"Docker check failed for '{self.container_name}': {e}")
if "Cannot access Docker" in str(e) or "not found" in str(e): return False
raise
else:
self.logger.error(f"Error checking container: {e}")
raise Exception(f"Failed to check container existence: {str(e)}")
async def _check_wp_cli_available(self) -> bool: async def _check_wp_cli_available(self) -> bool:
""" """

View File

@@ -112,22 +112,36 @@ class WordPressAdvancedPlugin(BasePlugin):
Dict with health status and WP-CLI availability Dict with health status and WP-CLI availability
""" """
try: try:
# Test WP-CLI access (primary requirement for wordpress_advanced) # Test REST API access by hitting /wp-json/ directly
wp_cli_version = await self.system.wp_cli_version() # NOTE: self.client.get("/") hits /wp-json/wp/v2/ which doesn't
wp_cli_available = bool(wp_cli_version.get("version")) # return a "name" field. We need /wp-json/ for the site index.
# Test REST API access with a public endpoint
rest_api_available = False rest_api_available = False
try: try:
# Use a public endpoint that doesn't require authentication import aiohttp
site_info = await self.client.get("/")
rest_api_available = bool(site_info.get("name")) site_url = self.client.site_url
async with aiohttp.ClientSession() as session:
async with session.get(
f"{site_url}/wp-json/",
timeout=aiohttp.ClientTimeout(total=10),
ssl=False,
) as resp:
if resp.status == 200:
data = await resp.json()
rest_api_available = bool(data.get("name"))
except Exception as e: except Exception as e:
self.logger.warning(f"REST API check failed (non-critical): {e}") self.logger.warning(f"REST API check failed: {e}")
rest_api_available = False
# 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 { return {
"healthy": wp_cli_available, # Only WP-CLI is critical for wordpress_advanced "healthy": rest_api_available,
"wp_cli_available": wp_cli_available, "wp_cli_available": wp_cli_available,
"rest_api_available": rest_api_available, "rest_api_available": rest_api_available,
"features": { "features": {

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "mcphub-server" name = "mcphub-server"
version = "3.0.0" version = "3.0.3"
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"}
@@ -66,11 +66,15 @@ Changelog = "https://github.com/airano-ir/mcphub/releases"
requires = ["setuptools>=68.0", "wheel"] requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.setuptools]
py-modules = ["server", "server_multi"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["core*", "plugins*"] include = ["core*", "plugins*"]
exclude = ["tests*", "data*", "logs*", "temp*", "scripts*", "docs*"] exclude = ["tests*", "data*", "logs*", "temp*", "scripts*", "docs*"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
"core" = ["templates/**/*.html"]
"*" = ["*.html", "*.css", "*.js", "*.json"] "*" = ["*.html", "*.css", "*.js", "*.json"]
# ==================================== # ====================================

393
server.py
View File

@@ -1,16 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Coolify Projects MCP Server MCP Hub Server
Universal MCP server for managing Coolify projects through plugins. Universal MCP server for managing self-hosted services through plugins.
Supports WordPress, Supabase, Gitea, and custom project types. Supports WordPress, WooCommerce, Gitea, n8n, Supabase, OpenPanel, Appwrite, and Directus.
Usage: Usage:
# With stdio transport (Claude Desktop) # With stdio transport (Claude Desktop)
python server.py python server.py
# With SSE transport (HTTP server) # With HTTP transport (Streamable HTTP server)
python server.py --transport sse --port 8000 python server.py --transport streamable-http --port 8000
Environment Variables: Environment Variables:
MASTER_API_KEY: Master API key for authentication MASTER_API_KEY: Master API key for authentication
@@ -23,9 +23,17 @@ import logging
import os import os
import sys import sys
import time import time
import warnings
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Optional from typing import Optional
from dotenv import load_dotenv
load_dotenv() # Load .env from current working directory
# Suppress noisy deprecation warning from websockets (transitive dependency)
warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets")
from fastmcp import FastMCP from fastmcp import FastMCP
from fastmcp.exceptions import ToolError from fastmcp.exceptions import ToolError
from fastmcp.server.dependencies import get_http_headers from fastmcp.server.dependencies import get_http_headers
@@ -45,7 +53,7 @@ from core import (
get_auth_manager, get_auth_manager,
get_project_manager, get_project_manager,
get_rate_limiter, get_rate_limiter,
# Option B modules (new clean architecture) # Core architecture modules
get_site_manager, get_site_manager,
get_site_registry, get_site_registry,
get_tool_registry, get_tool_registry,
@@ -228,10 +236,17 @@ if OAUTH_AUTH_MODE == "trusted_domains":
logger.info(f"OAuth Trusted Domains: {', '.join(OAUTH_TRUSTED_DOMAINS)}") logger.info(f"OAuth Trusted Domains: {', '.join(OAUTH_TRUSTED_DOMAINS)}")
# Initialize MCP server # Initialize MCP server
mcp = FastMCP("Coolify Projects Manager") mcp = FastMCP("MCP Hub")
# Initialize Jinja2 templates (Phase E - OAuth Authorization Page) # Initialize Jinja2 templates
templates = Jinja2Templates(directory="templates") # Templates live in core/templates/ (included in pip package as package_data)
_TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "core", "templates")
if not os.path.isdir(_TEMPLATES_DIR):
# Fallback for pip-installed package: resolve relative to core package
import core as _core_pkg
_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(_core_pkg.__file__)), "templates")
templates = Jinja2Templates(directory=_TEMPLATES_DIR)
logger.info("Jinja2 template engine initialized") logger.info("Jinja2 template engine initialized")
# Initialize managers # Initialize managers
@@ -239,7 +254,7 @@ auth_manager = get_auth_manager()
api_key_manager = get_api_key_manager() api_key_manager = get_api_key_manager()
project_manager = get_project_manager() project_manager = get_project_manager()
audit_logger = get_audit_logger() audit_logger = get_audit_logger()
csrf_manager = get_csrf_manager() # Phase E: CSRF protection csrf_manager = get_csrf_manager()
# Initialize site registry (legacy - kept for backward compatibility) # Initialize site registry (legacy - kept for backward compatibility)
site_registry = get_site_registry() site_registry = get_site_registry()
@@ -249,8 +264,7 @@ site_registry.discover_sites(plugin_types)
# Initialize unified tool generator (legacy - kept for backward compatibility) # Initialize unified tool generator (legacy - kept for backward compatibility)
unified_tool_generator = UnifiedToolGenerator(project_manager) unified_tool_generator = UnifiedToolGenerator(project_manager)
# === Option B Architecture (New Clean Architecture) === # Initialize site manager
# Initialize site manager (replacement for SiteRegistry)
site_manager = get_site_manager() site_manager = get_site_manager()
site_manager.discover_sites(plugin_types) site_manager.discover_sites(plugin_types)
@@ -261,10 +275,13 @@ tool_registry = get_tool_registry()
tool_generator = ToolGenerator(site_manager) tool_generator = ToolGenerator(site_manager)
logger.info("=" * 60) logger.info("=" * 60)
logger.info("Coolify Projects MCP Server - Option B Clean Architecture") logger.info("MCP Hub Server - Initialized")
logger.info("=" * 60) logger.info("=" * 60)
_mk = auth_manager.get_master_key() _mk = auth_manager.get_master_key()
logger.info(f"Master API Key: {_mk[:8]}***{_mk[-4:]}") if auth_manager._is_temporary_key:
logger.info(f"Master API Key (temporary): {_mk}")
else:
logger.info(f"Master API Key: {_mk[:8]}***{_mk[-4:]}")
logger.info(f"Discovered {len(project_manager.projects)} per-site project instances (legacy)") logger.info(f"Discovered {len(project_manager.projects)} per-site project instances (legacy)")
logger.info( logger.info(
f"Discovered {site_manager.get_count()} unique sites across {len(plugin_types)} plugin types" f"Discovered {site_manager.get_count()} unique sites across {len(plugin_types)} plugin types"
@@ -275,7 +292,7 @@ logger.info(f"Site breakdown: {site_manager.get_count_by_type()}")
for full_id, plugin in project_manager.projects.items(): for full_id, plugin in project_manager.projects.items():
logger.info(f" - {full_id} ({plugin.get_plugin_name()})") logger.info(f" - {full_id} ({plugin.get_plugin_name()})")
# Log discovered sites (Option B) # Log discovered sites
logger.info("\nDiscovered sites:") logger.info("\nDiscovered sites:")
for site_info in site_manager.list_all_sites(): for site_info in site_manager.list_all_sites():
alias_display = ( alias_display = (
@@ -457,7 +474,6 @@ def extract_plugin_type_from_tool(tool_name: str) -> str | None:
""" """
Extract plugin type from tool name for tool visibility filtering. Extract plugin type from tool name for tool visibility filtering.
Phase 5.5: Tool Visibility Filter
Each API key should only see tools related to its plugin type. Each API key should only see tools related to its plugin type.
Examples: Examples:
@@ -471,10 +487,11 @@ def extract_plugin_type_from_tool(tool_name: str) -> str | None:
Returns: Returns:
Plugin type string or None for system tools Plugin type string or None for system tools
""" """
# Remove MCP namespace prefix if present # Remove MCP namespace prefix if present (e.g., "mcp__mcp-hub__wordpress_...")
clean_name = tool_name clean_name = tool_name
if tool_name.startswith("mcp__coolify-projects__"): if tool_name.startswith("mcp__") and "__" in tool_name[5:]:
clean_name = tool_name.replace("mcp__coolify-projects__", "") # Strip "mcp__{server-name}__" prefix
clean_name = tool_name.split("__", 2)[-1]
# Check for plugin types (order matters - check more specific first) # Check for plugin types (order matters - check more specific first)
# wordpress_advanced must be checked before wordpress # wordpress_advanced must be checked before wordpress
@@ -505,8 +522,6 @@ def check_tool_visibility(tool_name: str, api_key_project_id: str) -> bool:
""" """
Check if API key has visibility to the requested tool. Check if API key has visibility to the requested tool.
Phase 5.5: Tool Visibility Filter
Rules: Rules:
- Global API keys (project_id="*") see ALL tools - Global API keys (project_id="*") see ALL tools
- Plugin-specific keys (project_id="wordpress_xxx") see only that plugin's tools - Plugin-specific keys (project_id="wordpress_xxx") see only that plugin's tools
@@ -683,15 +698,15 @@ class UserAuthMiddleware(Middleware):
else: else:
# All plugin tools that have a 'site' parameter are unified tools # All plugin tools that have a 'site' parameter are unified tools
# They defer project access check to execution time # They defer project access check to execution time
# Clean any MCP namespace prefix before checking
check_name = tool_name
if tool_name.startswith("mcp__") and "__" in tool_name[5:]:
check_name = tool_name.split("__", 2)[-1]
is_unified_tool = ( is_unified_tool = (
tool_name.startswith("wordpress_") check_name.startswith("wordpress_")
or tool_name.startswith("wordpress_advanced_") or check_name.startswith("wordpress_advanced_")
or tool_name.startswith("woocommerce_") or check_name.startswith("woocommerce_")
or tool_name.startswith("gitea_") or check_name.startswith("gitea_")
or tool_name.startswith("mcp__coolify-projects__wordpress_")
or tool_name.startswith("mcp__coolify-projects__wordpress_advanced_")
or tool_name.startswith("mcp__coolify-projects__woocommerce_")
or tool_name.startswith("mcp__coolify-projects__gitea_")
) )
logger.debug( logger.debug(
@@ -781,7 +796,6 @@ class UserAuthMiddleware(Middleware):
key = api_key_manager.keys.get(key_id) if key_id else None key = api_key_manager.keys.get(key_id) if key_id else None
if key: if key:
# Phase 5.5: Tool Visibility Filter
# Check if API key has visibility to this tool # Check if API key has visibility to this tool
if not check_tool_visibility(tool_name, key.project_id): if not check_tool_visibility(tool_name, key.project_id):
plugin_type = extract_plugin_type_from_tool(tool_name) plugin_type = extract_plugin_type_from_tool(tool_name)
@@ -939,16 +953,16 @@ mcp.add_middleware(AuditLoggingMiddleware())
logger.info("Audit logging middleware enabled") logger.info("Audit logging middleware enabled")
# === RATE LIMITING (Phase 7.3) === # === RATE LIMITING ===
# Initialize rate limiter # Initialize rate limiter
rate_limiter = get_rate_limiter() rate_limiter = get_rate_limiter()
logger.info("Rate limiter initialized (Phase 7.3)") logger.info("Rate limiter initialized")
class RateLimitMiddleware(Middleware): class RateLimitMiddleware(Middleware):
""" """
Middleware to enforce rate limiting for all tool calls (Phase 7.3). Middleware to enforce rate limiting for all tool calls.
Uses Token Bucket algorithm to prevent API abuse with multi-level limits: Uses Token Bucket algorithm to prevent API abuse with multi-level limits:
- Per minute: 60 requests (default) - Per minute: 60 requests (default)
@@ -992,9 +1006,11 @@ class RateLimitMiddleware(Middleware):
tool_name = params.name if hasattr(params, "name") else "unknown" tool_name = params.name if hasattr(params, "name") else "unknown"
# Determine plugin type from tool name # Determine plugin type from tool name
if tool_name.startswith("wordpress_") or tool_name.startswith( # Clean any MCP namespace prefix
"mcp__coolify-projects__wordpress_" tn = tool_name
): if tool_name.startswith("mcp__") and "__" in tool_name[5:]:
tn = tool_name.split("__", 2)[-1]
if tn.startswith("wordpress_"):
plugin_type = "wordpress" plugin_type = "wordpress"
elif tool_name.startswith("woocommerce_"): elif tool_name.startswith("woocommerce_"):
plugin_type = "woocommerce" plugin_type = "woocommerce"
@@ -1032,10 +1048,10 @@ class RateLimitMiddleware(Middleware):
# Add rate limiting middleware to MCP server # Add rate limiting middleware to MCP server
mcp.add_middleware(RateLimitMiddleware()) mcp.add_middleware(RateLimitMiddleware())
logger.info("Rate limiting middleware enabled (Phase 7.3)") logger.info("Rate limiting middleware enabled")
# === HEALTH MONITORING (Phase 7.2) === # === HEALTH MONITORING ===
from core import initialize_health_monitor from core import initialize_health_monitor
@@ -1043,15 +1059,16 @@ from core import initialize_health_monitor
health_monitor = initialize_health_monitor( health_monitor = initialize_health_monitor(
project_manager=project_manager, project_manager=project_manager,
audit_logger=audit_logger, audit_logger=audit_logger,
site_manager=site_manager,
metrics_retention_hours=24, metrics_retention_hours=24,
max_metrics_per_project=1000, max_metrics_per_project=1000,
) )
logger.info("Health monitor initialized (Phase 7.2)") logger.info("Health monitor initialized")
class HealthMetricsMiddleware(Middleware): class HealthMetricsMiddleware(Middleware):
""" """
Middleware to track health metrics for all tool calls (Phase 7.2). Middleware to track health metrics for all tool calls.
Tracks: Tracks:
- Response time - Response time
@@ -1140,7 +1157,7 @@ class HealthMetricsMiddleware(Middleware):
# Add health metrics middleware # Add health metrics middleware
mcp.add_middleware(HealthMetricsMiddleware()) mcp.add_middleware(HealthMetricsMiddleware())
logger.info("Health metrics middleware enabled (Phase 7.2)") logger.info("Health metrics middleware enabled")
# === ENDPOINT MIDDLEWARE HELPER === # === ENDPOINT MIDDLEWARE HELPER ===
@@ -1168,12 +1185,29 @@ def add_endpoint_middleware(endpoint_mcp, endpoint_name: str = "unknown"):
# Internal implementation functions (not decorated) for system tools # Internal implementation functions (not decorated) for system tools
async def _list_projects_impl() -> str: async def _list_projects_impl() -> str:
"""Internal implementation for listing projects.""" """Internal implementation for listing projects (reads from SiteManager)."""
try: import json
projects = project_manager.list_projects()
result = {"total": len(projects), "projects": projects}
import json
try:
all_sites = site_manager.list_all_sites()
projects = []
for site_info in all_sites:
alias = site_info.get("alias")
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}
return json.dumps(result, indent=2) return json.dumps(result, indent=2)
except Exception as e: except Exception as e:
logger.error(f"Error listing projects: {e}", exc_info=True) logger.error(f"Error listing projects: {e}", exc_info=True)
@@ -1244,14 +1278,22 @@ async def get_project_info(project_id: str) -> str:
Returns: Returns:
JSON string with project information JSON string with project information
""" """
import json
try: try:
# Try legacy ProjectManager first
info = project_manager.get_project_info(project_id) 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: if info is None:
return f"Project '{project_id}' not found. Use list_projects to see available projects." return f"Project '{project_id}' not found. Use list_projects to see available projects."
import json
return json.dumps(info, indent=2) return json.dumps(info, indent=2)
except Exception as e: except Exception as e:
logger.error(f"Error getting project info: {e}", exc_info=True) logger.error(f"Error getting project info: {e}", exc_info=True)
@@ -1261,7 +1303,7 @@ async def get_project_info(project_id: str) -> str:
@mcp.tool() @mcp.tool()
async def check_all_projects_health() -> str: async def check_all_projects_health() -> str:
""" """
Check health status of all projects with enhanced metrics (Phase 7.2). Check health status of all projects with enhanced metrics.
Performs comprehensive health checks on all configured projects including: Performs comprehensive health checks on all configured projects including:
- Accessibility and response time - Accessibility and response time
@@ -1287,7 +1329,7 @@ async def check_all_projects_health() -> str:
@mcp.tool() @mcp.tool()
async def get_project_health(project_id: str) -> str: async def get_project_health(project_id: str) -> str:
""" """
Get detailed health information for a specific project (Phase 7.2). Get detailed health information for a specific project.
Args: Args:
project_id: Full project identifier (e.g., "wordpress_site1") project_id: Full project identifier (e.g., "wordpress_site1")
@@ -1314,7 +1356,7 @@ async def get_project_health(project_id: str) -> str:
@mcp.tool() @mcp.tool()
async def get_system_metrics() -> str: async def get_system_metrics() -> str:
""" """
Get overall MCP server metrics and statistics (Phase 7.2). Get overall MCP server metrics and statistics.
Returns system-wide metrics including: Returns system-wide metrics including:
- Uptime - Uptime
@@ -1340,7 +1382,7 @@ async def get_system_metrics() -> str:
@mcp.tool() @mcp.tool()
async def get_system_uptime() -> str: async def get_system_uptime() -> str:
""" """
Get MCP server uptime information (Phase 7.2). Get MCP server uptime information.
Returns: Returns:
JSON string with uptime in various formats (seconds, minutes, hours, days) JSON string with uptime in various formats (seconds, minutes, hours, days)
@@ -1359,7 +1401,7 @@ async def get_system_uptime() -> str:
@mcp.tool() @mcp.tool()
async def get_project_metrics(project_id: str, hours: int = 1) -> str: async def get_project_metrics(project_id: str, hours: int = 1) -> str:
""" """
Get historical metrics for a specific project (Phase 7.2). Get historical metrics for a specific project.
Args: Args:
project_id: Full project identifier (e.g., "wordpress_site1") project_id: Full project identifier (e.g., "wordpress_site1")
@@ -1389,7 +1431,7 @@ async def get_project_metrics(project_id: str, hours: int = 1) -> str:
@mcp.tool() @mcp.tool()
async def export_health_metrics(output_path: str = "logs/metrics_export.json") -> str: async def export_health_metrics(output_path: str = "logs/metrics_export.json") -> str:
""" """
Export all health metrics to a JSON file (Phase 7.2). Export all health metrics to a JSON file.
Args: Args:
output_path: Path to output file (default: logs/metrics_export.json) output_path: Path to output file (default: logs/metrics_export.json)
@@ -1444,7 +1486,7 @@ async def _reset_rate_limit_impl(client_id: str = None) -> str:
@mcp.tool() @mcp.tool()
async def get_rate_limit_stats(client_id: str = None) -> str: async def get_rate_limit_stats(client_id: str = None) -> str:
""" """
Get rate limiting statistics (Phase 7.3). Get rate limiting statistics.
Args: Args:
client_id: Optional client identifier to get specific client stats. client_id: Optional client identifier to get specific client stats.
@@ -1459,7 +1501,7 @@ async def get_rate_limit_stats(client_id: str = None) -> str:
@mcp.tool() @mcp.tool()
async def reset_rate_limit(client_id: str = None) -> str: async def reset_rate_limit(client_id: str = None) -> str:
""" """
Reset rate limit state for a client or all clients (Phase 7.3). Reset rate limit state for a client or all clients.
CAUTION: This is an administrative tool. Use with care. CAUTION: This is an administrative tool. Use with care.
@@ -1540,6 +1582,12 @@ def create_dynamic_tool(name: str, description: str, handler, input_schema: dict
) )
) )
# Sort params: required (no default) first, then optional (with default).
# Python/inspect.Signature requires non-default args before default args.
required_first = [p for p in params if p.default is inspect.Parameter.empty]
optional_after = [p for p in params if p.default is not inspect.Parameter.empty]
params = required_first + optional_after
# Create signature # Create signature
sig = inspect.Signature(params) sig = inspect.Signature(params)
@@ -1582,25 +1630,18 @@ def register_project_tools():
This function is called at startup to register all tools This function is called at startup to register all tools
from all discovered projects. from all discovered projects.
Option B Architecture (Phase 3 Complete):
- Uses ToolRegistry for centralized tool management
- Uses ToolGenerator for WordPress plugin (refactored in Phase 3)
- Type-safe with Pydantic models in ToolRegistry
- Tool specifications from plugin.get_tool_specifications()
Note: FastMCP requires using the @mcp.tool() decorator or mcp.tool(function) Note: FastMCP requires using the @mcp.tool() decorator or mcp.tool(function)
for tool registration. for tool registration.
""" """
logger.info("=" * 60) logger.info("=" * 60)
logger.info("TOOL REGISTRATION - Option B Architecture (Phase 3)") logger.info("TOOL REGISTRATION")
logger.info("=" * 60) logger.info("=" * 60)
# Phase 3: Use ToolGenerator for refactored plugins
logger.info("Generating tools with ToolGenerator...") logger.info("Generating tools with ToolGenerator...")
from plugins.wordpress.plugin import WordPressPlugin from plugins.wordpress.plugin import WordPressPlugin
# Generate tools for WordPress (refactored in Phase 3) # Generate tools for WordPress
logger.info("Generating WordPress tools from plugin specifications...") logger.info("Generating WordPress tools from plugin specifications...")
try: try:
wordpress_tools = tool_generator.generate_tools(WordPressPlugin, "wordpress") wordpress_tools = tool_generator.generate_tools(WordPressPlugin, "wordpress")
@@ -1796,16 +1837,18 @@ def register_project_tools():
logger.info("=" * 60) logger.info("=" * 60)
# System tools count (Phase 7.2 + 7.3 + API Keys): # System tools count:
# - 10 system/health/rate limit tools # - 10 health/monitoring/rate-limit tools
# - 4 system config tools (endpoints, system_info, audit_log, set_rate_limit_config)
# - 6 API key management tools # - 6 API key management tools
system_tools_count = 16 # - 4 OAuth management tools
system_tools_count = 24
total_tools = tool_registry.get_count() + system_tools_count total_tools = tool_registry.get_count() + system_tools_count
logger.info( logger.info(
f"Total tools available: {total_tools} ({tool_registry.get_count()} plugin + {system_tools_count} system)" f"Total tools available: {total_tools} ({tool_registry.get_count()} plugin + {system_tools_count} system)"
) )
logger.info("🎯 Option B Architecture: Tool count stays constant regardless of site count!") logger.info("Tool count stays constant regardless of site count")
return total_tools return total_tools
@@ -2814,7 +2857,7 @@ async def oauth_get_client_info(client_id: str) -> dict:
# === PHASE X.3: SYSTEM TOOLS === # === PHASE X.3: SYSTEM TOOLS ===
# Internal implementations for Phase X.3 system tools # Internal implementations for system tools
async def _get_endpoints_impl() -> dict: async def _get_endpoints_impl() -> dict:
"""Internal implementation for listing endpoints.""" """Internal implementation for listing endpoints."""
try: try:
@@ -2822,7 +2865,7 @@ async def _get_endpoints_impl() -> dict:
endpoints = [ endpoints = [
{ {
"path": "/mcp", "path": "/mcp",
"name": "Coolify Admin", "name": "MCP Hub Admin",
"description": "Full administrative access to all tools", "description": "Full administrative access to all tools",
"require_master_key": True, "require_master_key": True,
"plugin_types": ["all"], "plugin_types": ["all"],
@@ -2833,7 +2876,7 @@ async def _get_endpoints_impl() -> dict:
"description": "System management tools (API keys, OAuth, health, rate limiting)", "description": "System management tools (API keys, OAuth, health, rate limiting)",
"require_master_key": True, "require_master_key": True,
"plugin_types": ["system"], "plugin_types": ["system"],
"tool_count": 16, "tool_count": 24,
}, },
{ {
"path": "/wordpress/mcp", "path": "/wordpress/mcp",
@@ -3085,8 +3128,7 @@ async def health_check(request: Request) -> JSONResponse:
{ {
"status": "healthy", "status": "healthy",
"uptime": uptime_seconds, "uptime": uptime_seconds,
"projects": len(project_manager.projects), "sites": site_manager.get_count(),
"sites": site_manager.get_count(), # Option B
"tools": _total_tool_count, # Total tools (plugin + system) "tools": _total_tool_count, # Total tools (plugin + system)
"timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
} }
@@ -3320,23 +3362,26 @@ async def manage_api_keys_rotate(project_id: str) -> dict:
def create_system_mcp(): def create_system_mcp():
""" """
Create System-only MCP instance (Phase X.3). Create System-only MCP instance.
Contains only 16 system management tools: Contains 24 system management tools:
- API Key Management (6) - API Key Management (6): create, list, get_info, revoke, delete, rotate
- OAuth Management (3) - OAuth Management (4): register_client, list_clients, revoke_client, get_client_info
- Rate Limiting (3) - Rate Limiting (3): get_stats, reset, set_config
- Health & Status (4) - 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 from fastmcp import FastMCP
system_instructions = """This is the System Management endpoint (17 tools). system_instructions = """This is the System Management endpoint (24 tools).
Available tools: Available tools:
• API Key Management: create, list, get_info, revoke, delete, rotate • API Key Management: create, list, get_info, revoke, delete, rotate
• OAuth Management: register_client, list_clients, revoke_client, get_client_info • OAuth Management: register_client, list_clients, revoke_client, get_client_info
• Rate Limiting: get_stats, reset, set_config • 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 list_projects() to see all configured sites across all plugin types.
Use get_endpoints() to see all available MCP endpoints.""" Use get_endpoints() to see all available MCP endpoints."""
@@ -3447,7 +3492,98 @@ Use get_endpoints() to see all available MCP endpoints."""
requests_per_minute, requests_per_hour, requests_per_day requests_per_minute, requests_per_hour, requests_per_day
) )
logger.info("Created System endpoint with 17 tools") # Health & Monitoring tools (7)
@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."
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 return system_mcp
@@ -4040,8 +4176,54 @@ def create_multi_endpoint_app(transport: str = "streamable-http"):
}, },
) )
# Validate the token value (not just presence)
token = auth_header.removeprefix("Bearer ").strip()
if token and not self._is_valid_token(token):
base_url = get_oauth_base_url(request)
resource_metadata_url = f"{base_url}/.well-known/oauth-protected-resource"
logger.warning(f"MCP OAuth: Invalid token for {path}")
return Response(
content='{"error": "invalid_token", "error_description": "Bearer token is invalid"}',
status_code=401,
media_type="application/json",
headers={
"WWW-Authenticate": f'Bearer resource_metadata="{resource_metadata_url}", error="invalid_token"',
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
"Access-Control-Expose-Headers": "WWW-Authenticate",
},
)
return await call_next(request) return await call_next(request)
@staticmethod
def _is_valid_token(token: str) -> bool:
"""Check if a Bearer token is recognized by any auth backend."""
# 1. Master API key
if auth_manager.validate_master_key(token):
return True
# 2. Per-project API key (any valid key, skip project/scope check)
if token.startswith("cmp_"):
key_hash = api_key_manager._hash_key(token)
for key in api_key_manager.keys.values():
if key.key_hash == key_hash and key.is_valid():
return True
return False
# 3. OAuth JWT token
try:
from core.oauth import get_token_manager
token_manager = get_token_manager()
if token_manager.validate_access_token(token):
return True
except Exception:
pass
return False
# Create MCP instances # Create MCP instances
system_mcp = create_system_mcp() # Phase X.3 - System endpoint system_mcp = create_system_mcp() # Phase X.3 - System endpoint
wp_mcp = create_wordpress_mcp() wp_mcp = create_wordpress_mcp()
@@ -4172,12 +4354,18 @@ def create_multi_endpoint_app(transport: str = "streamable-http"):
except Exception as e: except Exception as e:
logger.warning(f"Error during lifespan cleanup for {name}: {e}") logger.warning(f"Error during lifespan cleanup for {name}: {e}")
# Root redirect: / → /dashboard (so users don't see 404)
async def root_redirect(request):
return RedirectResponse(url="/dashboard", status_code=302)
# Build routes # Build routes
# Note: Order matters! More specific routes first # Note: Order matters! More specific routes first
routes = [ routes = [
# Health check # Health check
Route("/health", health_check, methods=["GET"]), Route("/health", health_check, methods=["GET"]),
# Dashboard routes (Phase K.1) # Root redirect
Route("/", root_redirect, methods=["GET"]),
# Dashboard routes
Route("/dashboard/login", dashboard_login_page, methods=["GET"]), Route("/dashboard/login", dashboard_login_page, methods=["GET"]),
Route("/dashboard/login", dashboard_login_submit, methods=["POST"]), Route("/dashboard/login", dashboard_login_submit, methods=["POST"]),
Route("/dashboard/logout", dashboard_logout, methods=["GET", "POST"]), Route("/dashboard/logout", dashboard_logout, methods=["GET", "POST"]),
@@ -4258,6 +4446,13 @@ def create_multi_endpoint_app(transport: str = "streamable-http"):
safe_name = project_id.replace("-", "_").replace(".", "_") safe_name = project_id.replace("-", "_").replace(".", "_")
routes.append(Mount(mount_path, app=project_app, name=f"mcp_project_{safe_name}")) 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) # Main admin endpoint (must be last - catches all remaining routes)
routes.append(Mount("/", app=main_app, name="mcp_admin")) routes.append(Mount("/", app=main_app, name="mcp_admin"))
@@ -4269,21 +4464,21 @@ def create_multi_endpoint_app(transport: str = "streamable-http"):
app = Starlette(routes=routes, lifespan=combined_lifespan, middleware=middleware) app = Starlette(routes=routes, lifespan=combined_lifespan, middleware=middleware)
logger.info("=" * 60) logger.info("=" * 60)
logger.info("Multi-Endpoint Architecture (Phase K) Active") logger.info("Multi-Endpoint Architecture Active")
logger.info("=" * 60) logger.info("=" * 60)
logger.info("Dashboard: /dashboard") # Phase K logger.info("Dashboard: /dashboard")
logger.info("Endpoints:") logger.info("Endpoints:")
logger.info(" /mcp - Admin (all 589 tools)") logger.info(f" /mcp - Admin (all {_total_tool_count} tools)")
logger.info(" /system/mcp - System (17 tools)") logger.info(" /system/mcp - System (24 tools)")
logger.info(" /wordpress/mcp - WordPress Core (67 tools)") logger.info(" /wordpress/mcp - WordPress Core (67 tools)")
logger.info(" /woocommerce/mcp - WooCommerce (28 tools)") # Phase D.1 logger.info(" /woocommerce/mcp - WooCommerce (28 tools)")
logger.info(" /wordpress-advanced/mcp - WordPress Advanced (22 tools)") logger.info(" /wordpress-advanced/mcp - WordPress Advanced (22 tools)")
logger.info(" /gitea/mcp - Gitea (56 tools)") logger.info(" /gitea/mcp - Gitea (56 tools)")
logger.info(" /n8n/mcp - n8n Automation (56 tools)") # Phase F logger.info(" /n8n/mcp - n8n Automation (56 tools)")
logger.info(" /supabase/mcp - Supabase (70 tools)") # Phase G logger.info(" /supabase/mcp - Supabase (70 tools)")
logger.info(" /openpanel/mcp - OpenPanel Analytics (73 tools)") # Phase H logger.info(" /openpanel/mcp - OpenPanel Analytics (73 tools)")
logger.info(" /appwrite/mcp - Appwrite Backend (100 tools)") # Phase I logger.info(" /appwrite/mcp - Appwrite Backend (100 tools)")
logger.info(" /directus/mcp - Directus CMS (100 tools)") # Phase J logger.info(" /directus/mcp - Directus CMS (100 tools)")
# Log per-project endpoints # Log per-project endpoints
if project_apps: if project_apps:
@@ -4313,7 +4508,7 @@ def main():
import uvicorn import uvicorn
# Parse command line arguments for transport configuration # Parse command line arguments for transport configuration
parser = argparse.ArgumentParser(description="Coolify Projects MCP Server") parser = argparse.ArgumentParser(description="MCP Hub Server")
parser.add_argument( parser.add_argument(
"--transport", "--transport",
type=str, type=str,
@@ -4340,10 +4535,10 @@ def main():
logger.info(f"Host: {args.host}") logger.info(f"Host: {args.host}")
logger.info(f"Port: {args.port}") logger.info(f"Port: {args.port}")
# Check if any projects were discovered # Check if any sites were discovered (SiteManager is the primary discovery system)
if len(project_manager.projects) == 0: if site_manager.get_count() == 0:
logger.warning("No projects discovered! Check your environment variables.") logger.warning("No sites discovered! Check your environment variables.")
logger.warning("Expected format: {PLUGIN_TYPE}_{PROJECT_ID}_{CONFIG_KEY}=value") logger.warning("Expected format: {PLUGIN_TYPE}_{SITE_ID}_{CONFIG_KEY}=value")
logger.warning("Example: WORDPRESS_SITE1_URL=https://example.com") logger.warning("Example: WORDPRESS_SITE1_URL=https://example.com")
try: try:

View File

@@ -19,7 +19,7 @@ Benefits:
- Scalable architecture - Scalable architecture
Usage: Usage:
python server_multi.py --transport sse --port 8000 python server_multi.py --transport streamable-http --port 8000
""" """
import inspect import inspect

View File

@@ -64,22 +64,18 @@ class TestPluginDisplayNames:
class TestLanguageDetection: class TestLanguageDetection:
"""Test language detection from headers and query params.""" """Test language detection from query params (default English)."""
def test_default_english(self): def test_default_english(self):
"""Should default to English.""" """Should default to English."""
assert detect_language(None) == "en" assert detect_language(None) == "en"
def test_detect_farsi_from_header(self): def test_header_ignored(self):
"""Should detect Farsi from Accept-Language header.""" """Accept-Language header should be ignored (always default to English)."""
assert detect_language("fa-IR,fa;q=0.9,en;q=0.8") == "fa" assert detect_language("fa-IR,fa;q=0.9,en;q=0.8") == "en"
def test_detect_farsi_short(self): def test_query_param_farsi(self):
"""Should detect 'fa' in Accept-Language.""" """Should detect Farsi from explicit query parameter."""
assert detect_language("fa") == "fa"
def test_query_param_overrides_header(self):
"""Query parameter should override Accept-Language header."""
assert detect_language("en-US", query_lang="fa") == "fa" assert detect_language("en-US", query_lang="fa") == "fa"
def test_query_param_english(self): def test_query_param_english(self):
@@ -87,8 +83,8 @@ class TestLanguageDetection:
assert detect_language("fa-IR", query_lang="en") == "en" assert detect_language("fa-IR", query_lang="en") == "en"
def test_invalid_query_lang_ignored(self): def test_invalid_query_lang_ignored(self):
"""Invalid query lang should be ignored, fall back to header.""" """Invalid query lang should be ignored, default to English."""
assert detect_language("fa-IR", query_lang="de") == "fa" assert detect_language("fa-IR", query_lang="de") == "en"
def test_english_header(self): def test_english_header(self):
"""Should return English for English header.""" """Should return English for English header."""
@@ -181,6 +177,27 @@ class TestDashboardAuthValidation:
assert user_type == "master" assert user_type == "master"
assert key_id is None assert key_id is None
def test_temp_key_via_auth_manager(self, auth, monkeypatch):
"""Should accept temp key from AuthManager when no env MASTER_API_KEY."""
from core.auth import AuthManager
# Simulate AuthManager with a temp key that DashboardAuth doesn't know about
temp_key = "temp-generated-key-12345"
mgr = AuthManager.__new__(AuthManager)
mgr.master_api_key = temp_key
mgr._is_temporary_key = True
mgr.project_keys = {}
monkeypatch.setattr("core.auth.get_auth_manager", lambda: mgr)
# DashboardAuth has master_api_key=None (no env var)
auth_no_env = DashboardAuth(secret_key="test-secret", master_api_key=None)
is_valid, user_type, key_id = auth_no_env.validate_api_key(temp_key)
assert is_valid is True
assert user_type == "master"
assert key_id is None
def test_invalid_key_rejected(self, auth): def test_invalid_key_rejected(self, auth):
"""Should reject invalid API key.""" """Should reject invalid API key."""
is_valid, user_type, key_id = auth.validate_api_key("wrong-key") is_valid, user_type, key_id = auth.validate_api_key("wrong-key")

Binary file not shown.

View File

@@ -0,0 +1,106 @@
# OpenPanel Self-Hosted - WordPress Plugin
**Version:** 1.1.1
**Requires:** WordPress 5.8+, PHP 7.4+
**License:** GPLv2 or later
## Description
**OpenPanel Self-Hosted** is a fork of the [official OpenPanel WordPress plugin](https://wordpress.org/plugins/openpanel/) with full **Self-Hosted instance support**. [OpenPanel](https://openpanel.dev) is an open-source web and product analytics platform — a privacy-friendly alternative to Google Analytics.
This plugin seamlessly integrates OpenPanel (Cloud or Self-Hosted) with your WordPress site while maximizing reliability and avoiding ad-blocker interference.
Designed to work with [MCP Hub](https://github.com/airano-ir/mcphub) for AI-powered analytics management.
## Key Features
- **Self-Hosted Support**: Works with both OpenPanel Cloud and your own Self-Hosted instance
- **Ad-Blocker Resistant**: Serves analytics scripts and API calls from your own domain
- **Real-Time Analytics**: Instant insights without processing delays
- **Privacy-Friendly**: Cookie-less tracking — no cookie banners needed
- **Performance Optimized**: Local script caching and efficient proxying
- **Product Analytics**: Funnel analysis, retention tracking, conversion metrics
- **Web Analytics**: Visitors, referrals, top pages, devices, sessions, bounce rates
## How It Works
1. **Inlines** `op1.js` directly into your pages (cached locally for 1 week)
2. **Bootstraps** the OpenPanel SDK with your Client ID automatically
3. **Proxies** all SDK requests through WordPress REST API (`/wp-json/openpanel/`)
4. **Preserves** all request methods, headers, query parameters, and body data
Serving scripts and data from your own domain origin avoids third-party blocking and improves tracking reliability.
## Installation
### Method 1: Upload via WordPress Admin
1. Download `openpanel-self-hosted.zip` from [Releases](https://github.com/airano-ir/mcphub)
2. Go to WordPress Admin > Plugins > Add New > Upload Plugin
3. Upload the ZIP and click "Install Now"
4. Activate the plugin
### Method 2: Manual Upload
1. Upload the `openpanel-self-hosted` folder to `/wp-content/plugins/`
2. Activate via WordPress Admin > Plugins
## Configuration
### Cloud Mode (openpanel.dev)
1. Sign up at [OpenPanel.dev](https://openpanel.dev) and create a project
2. Go to **Settings > OpenPanel** in WordPress admin
3. Select **Cloud (openpanel.dev)** as hosting mode
4. Paste your **Client ID** (starts with `op_client_`)
5. Enable desired auto-tracking features
### Self-Hosted Mode
1. Go to **Settings > OpenPanel** in WordPress admin
2. Select **Self-Hosted** as hosting mode
3. Enter your **API URL** (e.g., `https://api.openpanel.yourdomain.com`)
4. Enter your **Dashboard URL** (e.g., `https://openpanel.yourdomain.com`)
5. Paste your **Client ID** from your self-hosted instance
6. Enable desired auto-tracking features
| Setting | Example Value |
|---------|---------------|
| API URL | `https://api.openpanel.yourdomain.com` |
| Dashboard URL | `https://openpanel.yourdomain.com` |
| Client ID | Your project's client ID from OpenPanel |
## Privacy
- **No Cookie Banners Required**: Cookie-less tracking technology
- **GDPR Friendly**: Compliant without requiring user consent for basic analytics
- **Data Ownership**: Full control over your analytics data
- **No PII Collection**: Tracks behavior patterns without personal information
## Changelog
### 1.1.1
- Fixed op1.js loading for self-hosted instances (was always loading from CDN)
- Both inline cache and external fallback now use the correct self-hosted URL
### 1.1.0
- Full self-hosted OpenPanel instance support
- Hosting mode selector: Cloud vs Self-Hosted
- Configurable API URL and Dashboard URL
- Dynamic proxy validation for custom domains
### 1.0.0
- Initial release with OpenPanel Cloud integration
- Automatic script inlining with local caching
- REST API proxy for ad-blocker resistant tracking
- Auto-tracking: page views, outgoing links, page attributes
## Support
- **Plugin issues:** [GitHub Issues](https://github.com/airano-ir/mcphub/issues)
- **OpenPanel platform:** [OpenPanel.dev](https://openpanel.dev)
- **MCP Hub:** [MCP Hub Documentation](https://github.com/airano-ir/mcphub)
## License
GPLv2 or later - See [LICENSE](https://www.gnu.org/licenses/gpl-2.0.html) for details

View File

@@ -1,14 +1,16 @@
<?php <?php
/** /**
* Plugin Name: OpenPanel * Plugin Name: OpenPanel Self-Hosted
* Description: Activate OpenPanel to start tracking your website. Supports both Cloud and Self-Hosted instances. * Plugin URI: https://github.com/airano-ir/mcphub
* Description: OpenPanel analytics with full Self-Hosted instance support. Ad-blocker resistant proxy, privacy-friendly cookie-less tracking. Fork of the official OpenPanel plugin with self-hosted enhancements.
* Version: 1.1.1 * Version: 1.1.1
* Author: OpenPanel / Airano * Author: OpenPanel / MCP Hub
* Author URI: https://github.com/airano-ir
* License: GPLv2 or later * License: GPLv2 or later
* Requires at least: 5.8 * Requires at least: 5.8
* Requires PHP: 7.4 * Requires PHP: 7.4
* Tested up to: 6.8 * Tested up to: 6.9
* Text Domain: openpanel * Text Domain: openpanel-self-hosted
*/ */
if (!defined('ABSPATH')) { exit; } if (!defined('ABSPATH')) { exit; }
@@ -52,55 +54,55 @@ final class OP_WP_Proxy {
]); ]);
// Section: Hosting Mode // Section: Hosting Mode
add_settings_section('op_hosting', __('Hosting Mode', 'openpanel'), function() { add_settings_section('op_hosting', __('Hosting Mode', 'openpanel-self-hosted'), function() {
echo '<p>' . esc_html__('Choose between OpenPanel Cloud or your own Self-Hosted instance.', 'openpanel') . '</p>'; echo '<p>' . esc_html__('Choose between OpenPanel Cloud or your own Self-Hosted instance.', 'openpanel-self-hosted') . '</p>';
}, self::OPTION_KEY); }, self::OPTION_KEY);
add_settings_field('hosting_mode', __('Mode', 'openpanel'), function() { add_settings_field('hosting_mode', __('Mode', 'openpanel-self-hosted'), function() {
$opts = get_option(self::OPTION_KEY); $opts = get_option(self::OPTION_KEY);
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud'; $mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
?> ?>
<label> <label>
<input type="radio" name="<?php echo esc_attr(self::OPTION_KEY); ?>[hosting_mode]" value="cloud" <?php checked($mode, 'cloud'); ?> class="op-hosting-mode"> <input type="radio" name="<?php echo esc_attr(self::OPTION_KEY); ?>[hosting_mode]" value="cloud" <?php checked($mode, 'cloud'); ?> class="op-hosting-mode">
<?php esc_html_e('Cloud (openpanel.dev)', 'openpanel'); ?> <?php esc_html_e('Cloud (openpanel.dev)', 'openpanel-self-hosted'); ?>
</label><br> </label><br>
<label> <label>
<input type="radio" name="<?php echo esc_attr(self::OPTION_KEY); ?>[hosting_mode]" value="selfhosted" <?php checked($mode, 'selfhosted'); ?> class="op-hosting-mode"> <input type="radio" name="<?php echo esc_attr(self::OPTION_KEY); ?>[hosting_mode]" value="selfhosted" <?php checked($mode, 'selfhosted'); ?> class="op-hosting-mode">
<?php esc_html_e('Self-Hosted', 'openpanel'); ?> <?php esc_html_e('Self-Hosted', 'openpanel-self-hosted'); ?>
</label> </label>
<p class="description"><?php esc_html_e('Select Self-Hosted if you run your own OpenPanel instance.', 'openpanel'); ?></p> <p class="description"><?php esc_html_e('Select Self-Hosted if you run your own OpenPanel instance.', 'openpanel-self-hosted'); ?></p>
<?php <?php
}, self::OPTION_KEY, 'op_hosting'); }, self::OPTION_KEY, 'op_hosting');
// Section: Self-Hosted URLs // Section: Self-Hosted URLs
add_settings_section('op_selfhosted', __('Self-Hosted Settings', 'openpanel'), function() { add_settings_section('op_selfhosted', __('Self-Hosted Settings', 'openpanel-self-hosted'), function() {
echo '<p>' . esc_html__('Configure your Self-Hosted OpenPanel URLs. Only required if using Self-Hosted mode.', 'openpanel') . '</p>'; echo '<p>' . esc_html__('Configure your Self-Hosted OpenPanel URLs. Only required if using Self-Hosted mode.', 'openpanel-self-hosted') . '</p>';
}, self::OPTION_KEY); }, self::OPTION_KEY);
add_settings_field('api_url', __('API URL', 'openpanel'), function() { add_settings_field('api_url', __('API URL', 'openpanel-self-hosted'), function() {
$opts = get_option(self::OPTION_KEY); $opts = get_option(self::OPTION_KEY);
printf('<input type="url" name="%s[api_url]" value="%s" class="regular-text op-selfhosted-field" placeholder="https://api.openpanel.yourdomain.com"/>', printf('<input type="url" name="%s[api_url]" value="%s" class="regular-text op-selfhosted-field" placeholder="https://api.openpanel.yourdomain.com"/>',
esc_attr(self::OPTION_KEY), esc_attr(self::OPTION_KEY),
isset($opts['api_url']) ? esc_attr($opts['api_url']) : '' isset($opts['api_url']) ? esc_attr($opts['api_url']) : ''
); );
echo '<p class="description">' . esc_html__('Your OpenPanel API endpoint (e.g., https://api.openpanel.yourdomain.com)', 'openpanel') . '</p>'; echo '<p class="description">' . esc_html__('Your OpenPanel API endpoint (e.g., https://api.openpanel.yourdomain.com)', 'openpanel-self-hosted') . '</p>';
}, self::OPTION_KEY, 'op_selfhosted'); }, self::OPTION_KEY, 'op_selfhosted');
add_settings_field('dashboard_url', __('Dashboard URL', 'openpanel'), function() { add_settings_field('dashboard_url', __('Dashboard URL', 'openpanel-self-hosted'), function() {
$opts = get_option(self::OPTION_KEY); $opts = get_option(self::OPTION_KEY);
printf('<input type="url" name="%s[dashboard_url]" value="%s" class="regular-text op-selfhosted-field" placeholder="https://openpanel.yourdomain.com"/>', printf('<input type="url" name="%s[dashboard_url]" value="%s" class="regular-text op-selfhosted-field" placeholder="https://openpanel.yourdomain.com"/>',
esc_attr(self::OPTION_KEY), esc_attr(self::OPTION_KEY),
isset($opts['dashboard_url']) ? esc_attr($opts['dashboard_url']) : '' isset($opts['dashboard_url']) ? esc_attr($opts['dashboard_url']) : ''
); );
echo '<p class="description">' . esc_html__('Your OpenPanel Dashboard URL — used to load op1.js from your server instead of the CDN (e.g., https://openpanel.yourdomain.com)', 'openpanel') . '</p>'; echo '<p class="description">' . esc_html__('Your OpenPanel Dashboard URL — used to load op1.js from your server instead of the CDN (e.g., https://openpanel.yourdomain.com)', 'openpanel-self-hosted') . '</p>';
}, self::OPTION_KEY, 'op_selfhosted'); }, self::OPTION_KEY, 'op_selfhosted');
// Section: Main Settings // Section: Main Settings
add_settings_section('op_main', __('OpenPanel Settings', 'openpanel'), function() { add_settings_section('op_main', __('OpenPanel Settings', 'openpanel-self-hosted'), function() {
echo '<p>' . esc_html__('Set your OpenPanel Client ID. The SDK and requests are served from your domain to avoid ad blockers.', 'openpanel') . '</p>'; echo '<p>' . esc_html__('Set your OpenPanel Client ID. The SDK and requests are served from your domain to avoid ad blockers.', 'openpanel-self-hosted') . '</p>';
}, self::OPTION_KEY); }, self::OPTION_KEY);
add_settings_field('client_id', __('Client ID', 'openpanel'), function() { add_settings_field('client_id', __('Client ID', 'openpanel-self-hosted'), function() {
$opts = get_option(self::OPTION_KEY); $opts = get_option(self::OPTION_KEY);
printf('<input type="text" name="%s[client_id]" value="%s" class="regular-text" placeholder="op_client_..."/>', printf('<input type="text" name="%s[client_id]" value="%s" class="regular-text" placeholder="op_client_..."/>',
esc_attr(self::OPTION_KEY), esc_attr(self::OPTION_KEY),
@@ -108,14 +110,14 @@ final class OP_WP_Proxy {
); );
}, self::OPTION_KEY, 'op_main'); }, self::OPTION_KEY, 'op_main');
add_settings_field('toggles', __('Auto-tracking (optional)', 'openpanel'), function() { add_settings_field('toggles', __('Auto-tracking (optional)', 'openpanel-self-hosted'), function() {
$o = get_option(self::OPTION_KEY); $o = get_option(self::OPTION_KEY);
// Default track_screen to true if not set // Default track_screen to true if not set
$track_screen = isset($o['track_screen']) ? !empty($o['track_screen']) : true; $track_screen = isset($o['track_screen']) ? !empty($o['track_screen']) : true;
?> ?>
<label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_screen]" <?php checked($track_screen); ?>> <?php esc_html_e('Track page views automatically', 'openpanel'); ?></label><br> <label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_screen]" <?php checked($track_screen); ?>> <?php esc_html_e('Track page views automatically', 'openpanel-self-hosted'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_outgoing]" <?php checked(!empty($o['track_outgoing'])); ?>> <?php esc_html_e('Track clicks on outgoing links', 'openpanel'); ?></label><br> <label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_outgoing]" <?php checked(!empty($o['track_outgoing'])); ?>> <?php esc_html_e('Track clicks on outgoing links', 'openpanel-self-hosted'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_attributes]" <?php checked(!empty($o['track_attributes'])); ?>> <?php esc_html_e('Track additional page attributes', 'openpanel'); ?></label> <label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_attributes]" <?php checked(!empty($o['track_attributes'])); ?>> <?php esc_html_e('Track additional page attributes', 'openpanel-self-hosted'); ?></label>
<?php <?php
}, self::OPTION_KEY, 'op_main'); }, self::OPTION_KEY, 'op_main');
} }
@@ -136,7 +138,7 @@ final class OP_WP_Proxy {
delete_transient(self::TRANSIENT_JS); delete_transient(self::TRANSIENT_JS);
add_action('admin_notices', function() { add_action('admin_notices', function() {
echo '<div class="notice notice-success is-dismissible"><p>' . echo '<div class="notice notice-success is-dismissible"><p>' .
esc_html__('OpenPanel cache cleared successfully. The latest op1.js will be fetched on the next page load.', 'openpanel') . esc_html__('OpenPanel cache cleared successfully. The latest op1.js will be fetched on the next page load.', 'openpanel-self-hosted') .
'</p></div>'; '</p></div>';
}); });
} }
@@ -153,18 +155,18 @@ final class OP_WP_Proxy {
<?php <?php
settings_fields(self::OPTION_KEY); settings_fields(self::OPTION_KEY);
do_settings_sections(self::OPTION_KEY); do_settings_sections(self::OPTION_KEY);
submit_button(__('Save Settings', 'openpanel')); submit_button(__('Save Settings', 'openpanel-self-hosted'));
?> ?>
</form> </form>
<hr style="margin: 2rem 0;"> <hr style="margin: 2rem 0;">
<h2><?php esc_html_e('Cache Management', 'openpanel'); ?></h2> <h2><?php esc_html_e('Cache Management', 'openpanel-self-hosted'); ?></h2>
<p><?php esc_html_e('Clear the cached op1.js file to force fetch the latest version from OpenPanel.', 'openpanel'); ?></p> <p><?php esc_html_e('Clear the cached op1.js file to force fetch the latest version from OpenPanel.', 'openpanel-self-hosted'); ?></p>
<form method="post" action=""> <form method="post" action="">
<?php wp_nonce_field('op_clear_cache_nonce'); ?> <?php wp_nonce_field('op_clear_cache_nonce'); ?>
<input type="submit" name="op_clear_cache" class="button button-secondary" value="<?php esc_attr_e('Clear Cache & Force Refresh', 'openpanel'); ?>"> <input type="submit" name="op_clear_cache" class="button button-secondary" value="<?php esc_attr_e('Clear Cache & Force Refresh', 'openpanel-self-hosted'); ?>">
</form> </form>
<?php <?php
@@ -178,42 +180,42 @@ final class OP_WP_Proxy {
if ($time_remaining > 0) { if ($time_remaining > 0) {
echo '<p style="margin-top:1rem;color:#666;">' . echo '<p style="margin-top:1rem;color:#666;">' .
/* translators: %s: human readable time difference */ /* translators: %s: human readable time difference */
sprintf(esc_html__('Cache expires in %s', 'openpanel'), esc_html(human_time_diff(time(), $cached_time))) . sprintf(esc_html__('Cache expires in %s', 'openpanel-self-hosted'), esc_html(human_time_diff(time(), $cached_time))) .
'</p>'; '</p>';
} else { } else {
echo '<p style="margin-top:1rem;color:#666;">' . echo '<p style="margin-top:1rem;color:#666;">' .
esc_html__('Cache has expired and will refresh on next page load.', 'openpanel') . esc_html__('Cache has expired and will refresh on next page load.', 'openpanel-self-hosted') .
'</p>'; '</p>';
} }
} else { } else {
echo '<p style="margin-top:1rem;color:#666;">' . echo '<p style="margin-top:1rem;color:#666;">' .
esc_html__('No cached version found. op1.js will be fetched on next page load.', 'openpanel') . esc_html__('No cached version found. op1.js will be fetched on next page load.', 'openpanel-self-hosted') .
'</p>'; '</p>';
} }
?> ?>
<p style="margin-top:1rem;color:#666;"> <p style="margin-top:1rem;color:#666;">
<?php esc_html_e('The plugin fetches and inlines op1.js (cached for 1 week). If fetching fails, it falls back to the CDN script.', 'openpanel'); ?> <?php esc_html_e('The plugin fetches and inlines op1.js (cached for 1 week). If fetching fails, it falls back to the CDN script.', 'openpanel-self-hosted'); ?>
</p> </p>
<hr style="margin: 2rem 0;"> <hr style="margin: 2rem 0;">
<h2><?php esc_html_e('Current Configuration', 'openpanel'); ?></h2> <h2><?php esc_html_e('Current Configuration', 'openpanel-self-hosted'); ?></h2>
<table class="widefat" style="max-width: 600px;"> <table class="widefat" style="max-width: 600px;">
<tr> <tr>
<td><strong><?php esc_html_e('Mode', 'openpanel'); ?></strong></td> <td><strong><?php esc_html_e('Mode', 'openpanel-self-hosted'); ?></strong></td>
<td><?php echo esc_html($mode === 'selfhosted' ? 'Self-Hosted' : 'Cloud'); ?></td> <td><?php echo esc_html($mode === 'selfhosted' ? 'Self-Hosted' : 'Cloud'); ?></td>
</tr> </tr>
<tr> <tr>
<td><strong><?php esc_html_e('API URL', 'openpanel'); ?></strong></td> <td><strong><?php esc_html_e('API URL', 'openpanel-self-hosted'); ?></strong></td>
<td><code><?php echo esc_html($this->get_api_base()); ?></code></td> <td><code><?php echo esc_html($this->get_api_base()); ?></code></td>
</tr> </tr>
<tr> <tr>
<td><strong><?php esc_html_e('JS URL', 'openpanel'); ?></strong></td> <td><strong><?php esc_html_e('JS URL', 'openpanel-self-hosted'); ?></strong></td>
<td><code><?php echo esc_html($this->get_js_url()); ?></code></td> <td><code><?php echo esc_html($this->get_js_url()); ?></code></td>
</tr> </tr>
<tr> <tr>
<td><strong><?php esc_html_e('Proxy Endpoint', 'openpanel'); ?></strong></td> <td><strong><?php esc_html_e('Proxy Endpoint', 'openpanel-self-hosted'); ?></strong></td>
<td><code><?php echo esc_html(rest_url(self::REST_NS . '/')); ?></code></td> <td><code><?php echo esc_html(rest_url(self::REST_NS . '/')); ?></code></td>
</tr> </tr>
</table> </table>
@@ -479,7 +481,7 @@ final class OP_WP_Proxy {
// Content-Type is a special header NOT prefixed with HTTP_ in PHP // Content-Type is a special header NOT prefixed with HTTP_ in PHP
// This is critical for OpenPanel API which requires application/json // This is critical for OpenPanel API which requires application/json
if (!empty($_SERVER['CONTENT_TYPE'])) { 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) { foreach ($_SERVER as $name => $value) {

View File

@@ -1,18 +1,20 @@
=== OpenPanel === === OpenPanel Self-Hosted ===
Contributors: openpanel, airano 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 Requires at least: 5.8
Tested up to: 6.8 Tested up to: 6.9
Requires PHP: 7.4 Requires PHP: 7.4
Stable tag: 1.1.1 Stable tag: 1.1.1
License: GPLv2 or later License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html 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 == == Description ==
**OpenPanel** is an open-source web and product analytics platform that serves as a privacy-friendly alternative to traditional analytics solutions. This WordPress plugin seamlessly integrates [OpenPanel](https://openpanel.dev) with your WordPress site while maximizing reliability and avoiding ad-blocker interference. **OpenPanel Self-Hosted** is a fork of the [official OpenPanel WordPress plugin](https://wordpress.org/plugins/openpanel/) with full **Self-Hosted instance support**. [OpenPanel](https://openpanel.dev) is an open-source web and product analytics platform a privacy-friendly alternative to Google Analytics. This plugin seamlessly integrates OpenPanel (Cloud or Self-Hosted) with your WordPress site while maximizing reliability and avoiding ad-blocker interference.
Designed to work with [MCP Hub](https://github.com/airano-ir/mcphub) for AI-powered analytics management.
= Key Features = = Key Features =

Binary file not shown.

Binary file not shown.

View File

@@ -2,7 +2,7 @@
**Version:** 1.3.0 **Version:** 1.3.0
**Requires:** WordPress 5.0+, PHP 7.4+ **Requires:** WordPress 5.0+, PHP 7.4+
**License:** MIT **License:** GPLv2 or later
## Description ## Description
@@ -10,94 +10,82 @@ SEO API Bridge is a comprehensive WordPress plugin that exposes Rank Math SEO an
## What's New in 1.3.0 ## What's New in 1.3.0
**Full REST API Endpoints** - GET and POST operations for all post types - **Full REST API Endpoints** - GET and POST operations for all post types
**Product SEO Support** - Complete WooCommerce product SEO management - **Product SEO Support** - Complete WooCommerce product SEO management
**Simplified Integration** - Direct API calls instead of meta field manipulation - **Simplified Integration** - Direct API calls instead of meta field manipulation
**Better Error Handling** - Proper validation and error responses - **Better Error Handling** - Proper validation and error responses
## Features ## Features
- **Rank Math SEO Support** - Full access to all Rank Math meta fields - **Rank Math SEO Support** - Full access to all Rank Math meta fields
- **Yoast SEO Support** - Full access to all Yoast SEO meta fields - **Yoast SEO Support** - Full access to all Yoast SEO meta fields
- **WooCommerce Compatible** - Works with product post types - **WooCommerce Compatible** - Works with product post types
- **Secure** - Requires proper authentication and edit_posts capability - **Secure** - Requires proper authentication and edit_posts capability
- **Auto-Detection** - Automatically detects which SEO plugin is active - **Auto-Detection** - Automatically detects which SEO plugin is active
- **Health Check Endpoint** - NEW! Dedicated API endpoint for plugin detection - **Health Check Endpoint** - Dedicated API endpoint for plugin detection
- **Zero Configuration** - Works out of the box after activation - **Zero Configuration** - Works out of the box after activation
## Supported SEO Fields ## Supported SEO Fields
### Rank Math SEO ### Rank Math SEO
#### Core Fields | Category | Field | Description |
- `rank_math_focus_keyword` - Focus keyword |----------|-------|-------------|
- `rank_math_seo_title` - Meta title | Core | `rank_math_focus_keyword` | Focus keyword |
- `rank_math_description` - Meta description | Core | `rank_math_title` | Meta title |
- `rank_math_additional_keywords` - Additional keywords | Core | `rank_math_description` | Meta description |
| Core | `rank_math_additional_keywords` | Additional keywords |
#### Advanced Fields | Advanced | `rank_math_canonical_url` | Canonical URL |
- `rank_math_canonical_url` - Canonical URL | Advanced | `rank_math_robots` | Robots meta directives |
- `rank_math_robots` - Robots meta directives | Advanced | `rank_math_breadcrumb_title` | Breadcrumb title |
- `rank_math_breadcrumb_title` - Breadcrumb title | Open Graph | `rank_math_facebook_title` | OG title |
| Open Graph | `rank_math_facebook_description` | OG description |
#### Open Graph (Facebook) | Open Graph | `rank_math_facebook_image` | OG image URL |
- `rank_math_facebook_title` - OG title | Open Graph | `rank_math_facebook_image_id` | OG image ID |
- `rank_math_facebook_description` - OG description | Twitter | `rank_math_twitter_title` | Twitter title |
- `rank_math_facebook_image` - OG image URL | Twitter | `rank_math_twitter_description` | Twitter description |
- `rank_math_facebook_image_id` - OG image ID | Twitter | `rank_math_twitter_image` | Twitter image URL |
| Twitter | `rank_math_twitter_image_id` | Twitter image ID |
#### Twitter Card | Twitter | `rank_math_twitter_card_type` | Card type (summary, summary_large_image) |
- `rank_math_twitter_title` - Twitter title
- `rank_math_twitter_description` - Twitter description
- `rank_math_twitter_image` - Twitter image URL
- `rank_math_twitter_image_id` - Twitter image ID
- `rank_math_twitter_card_type` - Card type (summary, summary_large_image)
### Yoast SEO ### Yoast SEO
#### Core Fields | Category | Field | Description |
- `_yoast_wpseo_focuskw` - Focus keyword |----------|-------|-------------|
- `_yoast_wpseo_title` - Meta title | Core | `_yoast_wpseo_focuskw` | Focus keyword |
- `_yoast_wpseo_metadesc` - Meta description | Core | `_yoast_wpseo_title` | Meta title |
| Core | `_yoast_wpseo_metadesc` | Meta description |
#### Advanced Fields | Advanced | `_yoast_wpseo_canonical` | Canonical URL |
- `_yoast_wpseo_canonical` - Canonical URL | Advanced | `_yoast_wpseo_meta-robots-noindex` | Noindex setting |
- `_yoast_wpseo_meta-robots-noindex` - Noindex setting | Advanced | `_yoast_wpseo_meta-robots-nofollow` | Nofollow setting |
- `_yoast_wpseo_meta-robots-nofollow` - Nofollow setting | Advanced | `_yoast_wpseo_bctitle` | Breadcrumb title |
- `_yoast_wpseo_bctitle` - Breadcrumb title | Open Graph | `_yoast_wpseo_opengraph-title` | OG title |
| Open Graph | `_yoast_wpseo_opengraph-description` | OG description |
#### Open Graph | Open Graph | `_yoast_wpseo_opengraph-image` | OG image URL |
- `_yoast_wpseo_opengraph-title` - OG title | Open Graph | `_yoast_wpseo_opengraph-image-id` | OG image ID |
- `_yoast_wpseo_opengraph-description` - OG description | Twitter | `_yoast_wpseo_twitter-title` | Twitter title |
- `_yoast_wpseo_opengraph-image` - OG image URL | Twitter | `_yoast_wpseo_twitter-description` | Twitter description |
- `_yoast_wpseo_opengraph-image-id` - OG image ID | Twitter | `_yoast_wpseo_twitter-image` | Twitter image URL |
| Twitter | `_yoast_wpseo_twitter-image-id` | Twitter image ID |
#### Twitter Card
- `_yoast_wpseo_twitter-title` - Twitter title
- `_yoast_wpseo_twitter-description` - Twitter description
- `_yoast_wpseo_twitter-image` - Twitter image URL
- `_yoast_wpseo_twitter-image-id` - Twitter image ID
## Installation ## Installation
### Method 1: Manual Upload via WordPress Admin ### Method 1: Upload via WordPress Admin
1. Download the plugin folder 1. Download `seo-api-bridge.zip` from [Releases](https://github.com/airano-ir/mcphub)
2. Create a ZIP file: `seo-api-bridge.zip` 2. Go to WordPress Admin > Plugins > Add New > Upload Plugin
3. Go to WordPress Admin → Plugins → Add New → Upload Plugin 3. Upload the ZIP file and click "Install Now"
4. Upload the ZIP file and click "Install Now" 4. Click "Activate Plugin"
5. Click "Activate Plugin"
### Method 2: FTP/SSH Upload ### Method 2: FTP/SSH Upload
1. Upload the `seo-api-bridge` folder to `/wp-content/plugins/` 1. Upload the `seo-api-bridge` folder to `/wp-content/plugins/`
2. Go to WordPress Admin Plugins 2. Go to WordPress Admin > Plugins
3. Find "SEO API Bridge" and click "Activate" 3. Find "SEO API Bridge" and click "Activate"
### Method 3: WP-CLI (Recommended for Coolify) ### Method 3: WP-CLI
```bash ```bash
# Via SSH to your WordPress container
cd /var/www/html/wp-content/plugins/ cd /var/www/html/wp-content/plugins/
# Copy the plugin folder here # Copy the plugin folder here
wp plugin activate seo-api-bridge wp plugin activate seo-api-bridge
@@ -105,55 +93,92 @@ wp plugin activate seo-api-bridge
## REST API Endpoints ## REST API Endpoints
All endpoints require **WordPress Application Password** authentication.
### Status Endpoint ### Status Endpoint
**GET** `/wp-json/seo-bridge/v1/status` **GET** `/wp-json/seo-api-bridge/v1/status`
Check plugin status and detected SEO plugins.
```bash ```bash
curl https://yoursite.com/wp-json/seo-bridge/v1/status curl -X GET "https://yoursite.com/wp-json/seo-api-bridge/v1/status" \
-u "username:application_password"
```
**Response:**
```json
{
"plugin": "SEO API Bridge",
"version": "1.3.0",
"active": true,
"seo_plugins": {
"rank_math": { "active": true, "version": "1.0.257" },
"yoast": { "active": false, "version": null }
},
"supported_post_types": ["post", "page", "product"],
"message": "SEO API Bridge is active and working with Rank Math SEO."
}
``` ```
### Post SEO Endpoints ### Post SEO Endpoints
**GET** `/wp-json/seo-bridge/v1/posts/{id}/seo` **GET** `/wp-json/seo-api-bridge/v1/posts/{id}/seo` — Get SEO metadata for a post
Get SEO metadata for a post. **POST** `/wp-json/seo-api-bridge/v1/posts/{id}/seo` — Update SEO metadata for a post
**POST** `/wp-json/seo-bridge/v1/posts/{id}/seo`
Update SEO metadata for a post.
```bash ```bash
curl -X POST https://yoursite.com/wp-json/seo-bridge/v1/posts/123/seo \ # Get post SEO
curl -X GET "https://yoursite.com/wp-json/seo-api-bridge/v1/posts/123/seo" \
-u "username:application_password"
# Update post SEO
curl -X POST "https://yoursite.com/wp-json/seo-api-bridge/v1/posts/123/seo" \
-u "username:application_password" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"focus_keyword": "wordpress", "seo_title": "My Title"}' -d '{"focus_keyword": "wordpress", "seo_title": "My Title"}'
``` ```
### Page SEO Endpoints ### Page SEO Endpoints
**GET** `/wp-json/seo-bridge/v1/pages/{id}/seo` **GET** `/wp-json/seo-api-bridge/v1/pages/{id}/seo`
**POST** `/wp-json/seo-bridge/v1/pages/{id}/seo` **POST** `/wp-json/seo-api-bridge/v1/pages/{id}/seo`
### Product SEO Endpoints (WooCommerce) ### Product SEO Endpoints (WooCommerce)
**GET** `/wp-json/seo-bridge/v1/products/{id}/seo` **GET** `/wp-json/seo-api-bridge/v1/products/{id}/seo`
Get SEO metadata for a WooCommerce product. **POST** `/wp-json/seo-api-bridge/v1/products/{id}/seo`
**POST** `/wp-json/seo-bridge/v1/products/{id}/seo`
Update SEO metadata for a WooCommerce product.
```bash ```bash
curl -X POST https://yoursite.com/wp-json/seo-bridge/v1/products/1217/seo \ curl -X POST "https://yoursite.com/wp-json/seo-api-bridge/v1/products/1217/seo" \
-u "username:application_password" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"focus_keyword": "product keyword", "seo_title": "Product Title"}' -d '{"focus_keyword": "product keyword", "seo_title": "Product Title"}'
``` ```
## Usage with MCP Servers ### Alternative: Standard WordPress REST API
The plugin also registers all SEO meta fields on standard WordPress REST API endpoints. You can read/write SEO data directly via:
- `GET/POST /wp-json/wp/v2/posts/{id}` — SEO fields in the `meta` object
- `GET/POST /wp-json/wp/v2/pages/{id}`
- `GET/POST /wp-json/wp/v2/products/{id}` (WooCommerce)
```bash
# Read SEO fields via standard endpoint
curl -X GET "https://yoursite.com/wp-json/wp/v2/posts/123" \
-u "username:application_password"
# Update SEO via standard endpoint
curl -X POST "https://yoursite.com/wp-json/wp/v2/posts/123" \
-u "username:application_password" \
-H "Content-Type: application/json" \
-d '{"meta": {"rank_math_focus_keyword": "wordpress optimization"}}'
```
## Usage with MCP Hub
This plugin is designed to work with [MCP Hub](https://github.com/airano-ir/mcphub).
```javascript ```javascript
// Get product SEO // Get product SEO
@@ -172,244 +197,98 @@ await mcp.wordpress_update_product_seo({
}); });
``` ```
**Benefits of this approach:**
1.**WordPress Best Practice** - Uses recommended core REST API functionality
2.**Zero Maintenance** - Leverages WordPress built-in features
3.**Universal Compatibility** - Works with all WordPress REST API clients
4.**Inherits Security** - Uses WordPress authentication and permissions
5.**Simpler Code** - No custom endpoints to maintain
**Standard Endpoints:**
- Posts/Pages: `/wp-json/wp/v2/posts/{id}` or `/wp-json/wp/v2/pages/{id}`
- Products: `/wp-json/wp/v2/products/{id}` (WooCommerce custom post type)
**SEO data is in the `meta` object:**
```json
{
"id": 123,
"title": {"rendered": "My Post"},
"meta": {
"rank_math_focus_keyword": "wordpress seo",
"rank_math_seo_title": "Complete SEO Guide",
"rank_math_description": "Learn WordPress SEO best practices..."
}
}
```
### Reading SEO Data
**Get Post with SEO Fields:**
```bash
GET /wp-json/wp/v2/posts/{id}
```
**Response includes:**
```json
{
"id": 123,
"title": {"rendered": "My Post"},
"meta": {
"rank_math_focus_keyword": "wordpress seo",
"rank_math_seo_title": "Complete WordPress SEO Guide",
"rank_math_description": "Learn how to optimize your WordPress site...",
"rank_math_facebook_title": "SEO Guide for Facebook",
"rank_math_twitter_card_type": "summary_large_image"
}
}
```
### Writing SEO Data
**Update Post SEO Fields:**
```bash
POST /wp-json/wp/v2/posts/{id}
Authorization: Basic [base64(username:application_password)]
Content-Type: application/json
{
"meta": {
"rank_math_focus_keyword": "wordpress optimization",
"rank_math_seo_title": "WordPress Optimization Tips",
"rank_math_description": "Discover the best practices for WordPress optimization"
}
}
```
### WooCommerce Products
**Get Product with SEO:**
```bash
GET /wp-json/wp/v2/products/{id}
```
**Update Product SEO:**
```bash
POST /wp-json/wp/v2/products/{id}
{
"meta": {
"rank_math_focus_keyword": "premium widget",
"rank_math_description": "Buy the best premium widget online"
}
}
```
## Usage with MCP Server
This plugin is designed to work with the [Coolify Projects MCP Server](https://github.com/your-repo/mcphub).
### Example MCP Tool Usage
```python ```python
# Get post SEO data # Get post SEO data
result = await mcp.call_tool( result = await mcp.call_tool(
"wordpress_site1_get_post_seo", "wordpress_get_post_seo",
{"post_id": 123} {"site": "yoursite", "post_id": 123}
) )
# Update post SEO # Update post SEO
result = await mcp.call_tool( result = await mcp.call_tool(
"wordpress_site1_update_post_seo", "wordpress_update_post_seo",
{ {
"site": "yoursite",
"post_id": 123, "post_id": 123,
"focus_keyword": "wordpress seo", "focus_keyword": "wordpress seo",
"seo_title": "Complete SEO Guide", "seo_title": "Complete SEO Guide",
"meta_description": "Learn WordPress SEO..." "meta_description": "Learn WordPress SEO..."
} }
) )
# Update WooCommerce product SEO
result = await mcp.call_tool(
"wordpress_site1_update_product_seo",
{
"product_id": 456,
"focus_keyword": "premium widget",
"meta_description": "Buy the best widget"
}
)
``` ```
## Verification ## Verification
### Check if Plugin is Working 1. **Admin Notice:** After activation, visit the Plugins page to see a status notice indicating which SEO plugin was detected.
1. **Admin Notice:** After activation, you should see a success notice indicating which SEO plugin was detected 2. **Status Endpoint:**
2. **Health Check Endpoint (v1.1.0+):**
```bash ```bash
# Check plugin status directly
curl -X GET "https://your-site.com/wp-json/seo-api-bridge/v1/status" \ curl -X GET "https://your-site.com/wp-json/seo-api-bridge/v1/status" \
-u "username:application_password" -u "username:application_password"
``` ```
**Response:**
```json
{
"plugin": "SEO API Bridge",
"version": "1.1.0",
"active": true,
"seo_plugins": {
"rank_math": {
"active": true,
"version": "1.0.257"
},
"yoast": {
"active": false,
"version": null
}
},
"supported_post_types": ["post", "page", "product"],
"message": "SEO API Bridge is active and working with Rank Math SEO."
}
```
3. **REST API Test:** 3. **REST API Test:**
```bash ```bash
# Get any post and check if meta fields are present
curl -X GET "https://your-site.com/wp-json/wp/v2/posts/1" \ curl -X GET "https://your-site.com/wp-json/wp/v2/posts/1" \
-u "username:application_password" -u "username:application_password"
``` ```
4. **MCP Health Check:** The MCP server will automatically detect the plugin using the new endpoint 4. **MCP Health Check:** The MCP server automatically detects the plugin using the status endpoint.
## Security ## Security
- All meta fields require authentication via Application Passwords - All meta fields require authentication via Application Passwords
- Write access requires `edit_posts` capability - Write access requires `edit_posts` capability
- Read access follows WordPress post visibility rules - Read access follows WordPress post visibility rules
- No sensitive data exposed without proper permissions - No sensitive data exposed without proper permissions
## Troubleshooting ## Troubleshooting
### MCP Server Cannot Detect Plugin ### MCP Server Cannot Detect Plugin
**Issue:** MCP reports "SEO API Bridge plugin not detected" even though plugin is active 1. Upgrade to v1.1.0+ (has dedicated health check endpoint)
2. Restart MCP server
**Solution for v1.1.0+:** 3. Test endpoint: `curl "https://your-site.com/wp-json/seo-api-bridge/v1/status" -u "user:pass"`
1. **Upgrade to v1.1.0** - This version adds a dedicated health check endpoint
2. **Restart MCP server** - New detection logic will be used
3. **Test endpoint directly:**
```bash
curl "https://your-site.com/wp-json/seo-api-bridge/v1/status" -u "user:pass"
```
**Solution for v1.0.0 (legacy):**
1. Create at least one post or product with SEO metadata set
2. The old detection requires checking actual content meta fields
3. Upgrade to v1.1.0 to avoid this requirement
### SEO Plugin Not Detected ### SEO Plugin Not Detected
**Issue:** Admin notice says "Neither Rank Math SEO nor Yoast SEO is detected"
**Solution:**
1. Verify Rank Math or Yoast SEO is installed and activated 1. Verify Rank Math or Yoast SEO is installed and activated
2. Try deactivating and reactivating SEO API Bridge 2. Deactivate and reactivate SEO API Bridge
3. Check PHP error logs for any warnings 3. Check PHP error logs for warnings
### Meta Fields Not Appearing in REST API ### Meta Fields Not Appearing in REST API
**Issue:** `/wp-json/wp/v2/posts/{id}` doesn't show `meta` object
**Solution:**
1. Ensure you're authenticated (use Application Password) 1. Ensure you're authenticated (use Application Password)
2. Check that you have `edit_posts` permission 2. Check user has `edit_posts` permission
3. Verify the post type is supported (post, page, product) 3. Verify the post type is supported (post, page, product)
### Fields Are Read-Only
**Issue:** Can read SEO fields but cannot update them
**Solution:**
1. Verify authentication credentials
2. Check user has `edit_posts` capability
3. Ensure you're using POST/PUT request, not GET
## Compatibility ## Compatibility
- ✅ WordPress 5.0+ - WordPress 5.0+
- ✅ PHP 7.4+ - PHP 7.4+
- ✅ Rank Math SEO 1.0+ - Rank Math SEO 1.0+
- ✅ Yoast SEO 14.0+ - Yoast SEO 14.0+
- ✅ WooCommerce 5.0+ (for product support) - WooCommerce 5.0+ (for product support)
- ✅ Classic Editor & Gutenberg - Classic Editor & Gutenberg
- ✅ Multisite compatible - Multisite compatible
## Support
For issues related to:
- **This plugin:** [GitHub Issues](https://github.com/your-repo/mcphub/issues)
- **MCP Server:** [MCP Server Documentation](https://github.com/your-repo/mcphub)
- **Rank Math SEO:** [Rank Math Support](https://rankmath.com/support/)
- **Yoast SEO:** [Yoast Support](https://yoast.com/help/)
## Changelog ## Changelog
### 1.3.0 (2025-02-18)
- Added REST API endpoints for posts, pages, and products (GET/POST operations)
- Added authentication requirement for all endpoints
- Fixed `rank_math_title` meta key registration
- Fixed output escaping for WordPress.org compliance
### 1.2.0 (2025-02-15)
- Enhanced WooCommerce product support
- Improved MariaDB compatibility for meta field queries
### 1.1.0 (2025-01-10) ### 1.1.0 (2025-01-10)
- 🎉 **NEW:** Health check REST API endpoint `/seo-api-bridge/v1/status` - Added health check REST API endpoint `/seo-api-bridge/v1/status`
- 🎉 **NEW:** Direct plugin detection without requiring posts/products - Direct plugin detection without requiring posts/products
- 🎉 **IMPROVEMENT:** Better compatibility with sites that have no content - Better compatibility with sites that have no content
- 🎉 **IMPROVEMENT:** Returns SEO plugin versions in status endpoint - Returns SEO plugin versions in status endpoint
- 🎉 **FIX:** MCP server can now detect plugin even with zero posts
### 1.0.0 (2025-01-10) ### 1.0.0 (2025-01-10)
- Initial release - Initial release
@@ -417,12 +296,14 @@ For issues related to:
- Yoast SEO support (15 meta fields) - Yoast SEO support (15 meta fields)
- WooCommerce product support - WooCommerce product support
- Auto-detection of active SEO plugins - Auto-detection of active SEO plugins
- Admin notices for status
## Support
- **Plugin issues:** [GitHub Issues](https://github.com/airano-ir/mcphub/issues)
- **MCP Server:** [MCP Hub Documentation](https://github.com/airano-ir/mcphub)
- **Rank Math SEO:** [Rank Math Support](https://rankmath.com/support/)
- **Yoast SEO:** [Yoast Support](https://yoast.com/help/)
## License ## 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
Developed for use with Coolify Projects MCP Server to enable AI-powered SEO content management.

View File

@@ -0,0 +1,82 @@
=== SEO API Bridge ===
Contributors: airano
Tags: seo, rest-api, rank-math, yoast, mcp
Requires at least: 5.0
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 1.3.0
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.
== Description ==
SEO API Bridge is a WordPress plugin that exposes Rank Math SEO and Yoast SEO meta fields via dedicated REST API endpoints. This enables MCP servers, AI agents, and other applications to read and write SEO metadata programmatically for posts, pages, and WooCommerce products.
**Features:**
* **Rank Math SEO Support** — Full access to all Rank Math meta fields
* **Yoast SEO Support** — Full access to all Yoast SEO meta fields
* **WooCommerce Compatible** — Works with product post types
* **Secure** — Requires WordPress Application Password and edit_posts capability
* **Auto-Detection** — Automatically detects which SEO plugin is active
* **Status Endpoint** — Dedicated API endpoint for plugin and SEO detection
* **Zero Configuration** — Works out of the box after activation
**REST API Endpoints:**
* `GET/POST /wp-json/seo-api-bridge/v1/posts/{id}/seo` — Post SEO data
* `GET/POST /wp-json/seo-api-bridge/v1/pages/{id}/seo` — Page SEO data
* `GET/POST /wp-json/seo-api-bridge/v1/products/{id}/seo` — Product SEO data (WooCommerce)
* `GET /wp-json/seo-api-bridge/v1/status` — Plugin status and SEO detection
**Designed for [MCP Hub](https://github.com/airano-ir/mcphub)** — the AI-native management hub for WordPress and self-hosted services.
== Installation ==
1. Upload the `seo-api-bridge` folder to `/wp-content/plugins/`
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Ensure either Rank Math SEO or Yoast SEO is active
4. The REST API endpoints are now available
== Frequently Asked Questions ==
= Which SEO plugins are supported? =
Rank Math SEO and Yoast SEO. The plugin auto-detects which one is active.
= Does it work with WooCommerce? =
Yes. Product SEO endpoints are available when WooCommerce is active.
= How is authentication handled? =
All endpoints require WordPress Application Password authentication and the `edit_posts` capability. POST requests require the same capability.
= Can I use this without MCP Hub? =
Yes. The REST API endpoints work with any application that can make HTTP requests with proper WordPress authentication.
== Changelog ==
= 1.3.0 =
* Added REST API endpoints for posts, pages, and products (GET/POST operations)
* Added authentication requirement for all endpoints
* Fixed rank_math_title meta key registration
= 1.2.0 =
* Enhanced WooCommerce product support
* Improved MariaDB compatibility
= 1.1.0 =
* Added status endpoint for plugin detection
* Improved error handling
= 1.0.0 =
* Initial release with Rank Math and Yoast SEO support
== Upgrade Notice ==
= 1.3.0 =
GET endpoints now require authentication (edit_posts capability). Update your API clients if needed.

View File

@@ -1,12 +1,12 @@
<?php <?php
/** /**
* Plugin Name: SEO API Bridge * Plugin Name: SEO API Bridge
* Plugin URI: https://github.com/your-repo/seo-api-bridge * Plugin URI: https://github.com/airano-ir/mcphub
* Description: Exposes Rank Math SEO and Yoast SEO meta fields via WordPress REST API for use with MCP servers and AI agents. Supports posts, pages, and WooCommerce products with full CRUD operations. * Description: Exposes Rank Math SEO and Yoast SEO meta fields via WordPress REST API for use with MCP servers and AI agents. Supports posts, pages, and WooCommerce products with full CRUD operations.
* Version: 1.3.0 * Version: 1.3.0
* Author: MCP Coolify Projects * Author: MCP Hub
* Author URI: https://github.com/your-repo * Author URI: https://github.com/airano-ir
* License: MIT * License: GPL-2.0-or-later
* Requires at least: 5.0 * Requires at least: 5.0
* Requires PHP: 7.4 * Requires PHP: 7.4
* Text Domain: seo-api-bridge * Text Domain: seo-api-bridge
@@ -70,7 +70,9 @@ class SEO_API_Bridge {
[ [
'methods' => 'GET', 'methods' => 'GET',
'callback' => [$this, 'get_post_seo'], 'callback' => [$this, 'get_post_seo'],
'permission_callback' => '__return_true', 'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => [ 'args' => [
'id' => [ 'id' => [
'validate_callback' => function($param) { 'validate_callback' => function($param) {
@@ -100,7 +102,9 @@ class SEO_API_Bridge {
[ [
'methods' => 'GET', 'methods' => 'GET',
'callback' => [$this, 'get_page_seo'], 'callback' => [$this, 'get_page_seo'],
'permission_callback' => '__return_true', 'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => [ 'args' => [
'id' => [ 'id' => [
'validate_callback' => function($param) { 'validate_callback' => function($param) {
@@ -130,7 +134,9 @@ class SEO_API_Bridge {
[ [
'methods' => 'GET', 'methods' => 'GET',
'callback' => [$this, 'get_product_seo'], 'callback' => [$this, 'get_product_seo'],
'permission_callback' => '__return_true', 'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => [ 'args' => [
'id' => [ 'id' => [
'validate_callback' => function($param) { 'validate_callback' => function($param) {
@@ -246,7 +252,7 @@ class SEO_API_Bridge {
'description' => 'Rank Math focus keyword', 'description' => 'Rank Math focus keyword',
'single' => true, 'single' => true,
], ],
'rank_math_seo_title' => [ 'rank_math_title' => [
'type' => 'string', 'type' => 'string',
'description' => 'Rank Math SEO title (meta title)', 'description' => 'Rank Math SEO title (meta title)',
'single' => true, 'single' => true,
@@ -668,13 +674,19 @@ class SEO_API_Bridge {
* Display admin notices * Display admin notices
*/ */
public function admin_notices() { public function admin_notices() {
// Only show notices on the Plugins page to avoid clutter on every admin page
$screen = get_current_screen();
if ( ! $screen || $screen->id !== 'plugins' ) {
return;
}
$rank_math_active = $this->is_rank_math_active(); $rank_math_active = $this->is_rank_math_active();
$yoast_active = $this->is_yoast_active(); $yoast_active = $this->is_yoast_active();
$woocommerce_active = $this->is_woocommerce_active(); $woocommerce_active = $this->is_woocommerce_active();
if (!$rank_math_active && !$yoast_active) { if (!$rank_math_active && !$yoast_active) {
echo '<div class="notice notice-warning is-dismissible">'; echo '<div class="notice notice-warning is-dismissible">';
echo '<p><strong>SEO API Bridge:</strong> Neither Rank Math SEO nor Yoast SEO is detected. Please install and activate one of these plugins to enable SEO meta field access via REST API.</p>'; echo '<p><strong>SEO API Bridge:</strong> ' . esc_html__( 'Neither Rank Math SEO nor Yoast SEO is detected. Please install and activate one of these plugins to enable SEO meta field access via REST API.', 'seo-api-bridge' ) . '</p>';
echo '</div>'; echo '</div>';
} else { } else {
$active_plugins = []; $active_plugins = [];
@@ -684,11 +696,11 @@ class SEO_API_Bridge {
$supported_types = implode(', ', $this->supported_post_types); $supported_types = implode(', ', $this->supported_post_types);
echo '<div class="notice notice-success is-dismissible">'; echo '<div class="notice notice-success is-dismissible">';
echo '<p><strong>SEO API Bridge v' . self::VERSION . ':</strong> Successfully registered meta fields for ' . implode(' and ', $active_plugins) . '.</p>'; echo '<p><strong>SEO API Bridge v' . esc_html( self::VERSION ) . ':</strong> ' . esc_html( sprintf( 'Successfully registered meta fields for %s.', implode( ' and ', $active_plugins ) ) ) . '</p>';
echo '<p><strong>Supported post types:</strong> ' . $supported_types . '</p>'; echo '<p><strong>' . esc_html__( 'Supported post types:', 'seo-api-bridge' ) . '</strong> ' . esc_html( $supported_types ) . '</p>';
if ($woocommerce_active) { if ($woocommerce_active) {
echo '<p><strong>WooCommerce:</strong> Detected and supported. Product SEO fields are available via REST API.</p>'; echo '<p><strong>WooCommerce:</strong> ' . esc_html__( 'Detected and supported. Product SEO fields are available via REST API.', 'seo-api-bridge' ) . '</p>';
} }
echo '</div>'; echo '</div>';
} }