Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d97b31c12 | ||
|
|
72cfdad5e3 | ||
|
|
5ce70f120e | ||
|
|
cafcd64eff | ||
|
|
d74594524a | ||
|
|
b6551abe0a | ||
|
|
cb2a835d0e | ||
|
|
bb2a416dd5 | ||
|
|
ff79968170 | ||
|
|
bb851e4be2 | ||
|
|
91979ee18d | ||
|
|
809dc264af | ||
|
|
cb6bcd8136 | ||
|
|
3b87c01d78 | ||
|
|
85379ec36c | ||
|
|
4a45178d2d | ||
|
|
f303a04322 | ||
|
|
bb74703311 | ||
|
|
aa8afce707 | ||
|
|
076a0b940e | ||
|
|
7daf604a32 | ||
|
|
5e0441c85e | ||
|
|
3a3f04aca8 |
@@ -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
|
||||||
|
|||||||
12
CLAUDE.md
12
CLAUDE.md
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
175
DOCKER_README.md
Normal 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
|
||||||
@@ -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
161
README.md
@@ -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.
|
||||||
|
|
||||||
|
[](https://github.com/airano-ir/mcphub/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
[](https://pypi.org/project/mcphub-server/)
|
[](https://pypi.org/project/mcphub-server/)
|
||||||
[](https://hub.docker.com/r/airano/mcphub)
|
[](https://hub.docker.com/r/airano/mcphub)
|
||||||
[]()
|
[]()
|
||||||
[]()
|
[]()
|
||||||
[](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml)
|
[](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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
112
core/health.py
112
core/health.py
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Enhanced Health Monitoring System for MCP Server (Phase 7.2)
|
Enhanced Health Monitoring System for MCP Server
|
||||||
|
|
||||||
This module provides comprehensive health monitoring capabilities including:
|
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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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,21 +501,14 @@ class SiteManager:
|
|||||||
>>> suffix = manager.get_effective_path_suffix('wordpress_site1')
|
>>> suffix = manager.get_effective_path_suffix('wordpress_site1')
|
||||||
>>> print(suffix) # 'myblog' or 'wordpress_site1'
|
>>> print(suffix) # 'myblog' or 'wordpress_site1'
|
||||||
"""
|
"""
|
||||||
# Parse full_id to get plugin_type and site_id
|
# Look up by full_id from registered sites (handles multi-word plugin types)
|
||||||
parts = full_id.split("_", 1)
|
for info in self.list_all_sites():
|
||||||
if len(parts) != 2:
|
if info["full_id"] == full_id:
|
||||||
return full_id
|
config = self.sites[info["plugin_type"]].get(info["site_id"])
|
||||||
|
if config and config.alias and config.alias != config.site_id:
|
||||||
plugin_type, site_id = parts
|
|
||||||
|
|
||||||
# Try to get config
|
|
||||||
try:
|
|
||||||
config = self.get_site_config(plugin_type, site_id)
|
|
||||||
# If alias exists and is different from site_id, use alias
|
|
||||||
if config.alias and config.alias != config.site_id:
|
|
||||||
return config.alias
|
return config.alias
|
||||||
return full_id
|
return full_id
|
||||||
except ValueError:
|
|
||||||
return full_id
|
return full_id
|
||||||
|
|
||||||
def get_alias_conflicts(self) -> dict[str, list[str]]:
|
def get_alias_conflicts(self) -> dict[str, list[str]]:
|
||||||
|
|||||||
@@ -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."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
@@ -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,15 +109,22 @@
|
|||||||
<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 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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Language Toggle -->
|
<!-- Language Toggle -->
|
||||||
<div class="mt-6 text-center">
|
<div class="mt-6 text-center">
|
||||||
@@ -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>
|
||||||
1
core/templates/static/logo.svg
Normal file
1
core/templates/static/logo.svg
Normal 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 |
@@ -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}
|
||||||
|
|||||||
@@ -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
135
env.example
Normal 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
|
||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|
||||||
# ====================================
|
# ====================================
|
||||||
|
|||||||
391
server.py
391
server.py
@@ -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:
|
|
||||||
projects = project_manager.list_projects()
|
|
||||||
result = {"total": len(projects), "projects": projects}
|
|
||||||
import json
|
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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
BIN
wordpress-plugin/openpanel-self-hosted.zip
Normal file
BIN
wordpress-plugin/openpanel-self-hosted.zip
Normal file
Binary file not shown.
106
wordpress-plugin/openpanel-self-hosted/README.md
Normal file
106
wordpress-plugin/openpanel-self-hosted/README.md
Normal 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
|
||||||
@@ -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) {
|
||||||
@@ -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.
@@ -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.
|
|
||||||
|
|||||||
82
wordpress-plugin/seo-api-bridge/readme.txt
Normal file
82
wordpress-plugin/seo-api-bridge/readme.txt
Normal 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.
|
||||||
@@ -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>';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user