diff --git a/.gitignore b/.gitignore
index d48fc22..83ae5c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -165,3 +165,8 @@ pytest-cache-files-*/
worktrees/
/.agents
uv.lock
+
+# planning-with-files skill artifacts (session-local)
+task_plan.md
+findings.md
+progress.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5dcb916..e809aa3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,100 @@ All notable changes to MCP Hub will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [3.11.0] — 2026-04-14
+
+### Plugin-specific access levels, regression tests, UX polish (Track F.7d)
+
+Polish pass on top of v3.10.0 tool-access hardening. Access levels and credential guidance are now tailored per plugin; UX signals have been tightened.
+
+#### Added
+- **Plugin-aware scope presets** (`core/tool_access.py::get_scope_presets_for_plugin`): WordPress / WordPress Advanced / WooCommerce / Gitea / Coolify (5 tiers) / OpenPanel — each with tailored EN+FA labels and one-line hints. Custom preset always available.
+- **Admin Tools card** on per-service dashboard page (4-col grid when `admin` tools > 0).
+- **Credential requirement notice** on Tool Access: describes exactly what the saved Credential must grant, per plugin/tier (WP Application Password, WooCommerce REST key scope, Gitea scoped tokens, OpenPanel client mode, Coolify API token permissions).
+- **Auto-scoped quick key** on the Connect section (site_id injected → key capped to this site's tier).
+
+#### Changed
+- Scope buttons rendered dynamically from `scope_presets_json` (was: hardcoded 4 tiers).
+- WordPress / WP Advanced / WooCommerce: dropped redundant `write` tier (admin-scope tool count = 0) — now **Read Only + Full Access + Custom**.
+- Coolify tool list visually filtered by category for all 5 tiers in JS (mirrors `SCOPE_TO_CATEGORIES`).
+- OpenPanel Client ID hint clarifies the correct dashboard path (`{org}/{project-id}/settings/clients`).
+
+---
+
+## [3.10.0] — 2026-04-12
+
+### Tool access hardening + per-site API keys + Tailwind build (Track F.7c)
+
+#### Added
+- **Per-site API keys** (DB migration v8 adds `site_id` column): UI site selector in create modal; endpoint enforces site-scoped key access.
+- **Configuration Snippet section** on keys page (transport / WP / bearer notes).
+- **Pre-built Tailwind CSS** via `scripts/build-css.sh` (replaces CDN).
+- **Unified site manage page**: collapsible Connection / Tool Access / Connect sections.
+
+#### Fixed
+- **CSRF bug**: removed conflicting `getCsrf()` that tried to read an `httponly` cookie; now relies on the global meta-tag CSRF interceptor in `head_assets.html`.
+- **Tool scope UI**: tools outside the selected scope are visually dimmed; summary shows in-scope count.
+- Sidebar logo hidden when collapsed.
+
+#### Changed
+- **828 tests passing** (was: 766).
+- `/dashboard/connect` and `/dashboard/edit` redirect to unified site page.
+
+---
+
+## [3.9.0] — 2026-04-09
+
+### Tool access UI + unified keys page (Track F.7b)
+
+#### Added
+- `sites/edit`: Tool Access card with scope dropdown + per-tool toggle grid.
+- `sites/view`: new `/dashboard/sites/{id}` page with MCP URL + config snippets.
+- `/dashboard/keys`: unified page (user view with scope selector, admin view).
+- Sidebar: "Connect" replaced with "API Keys" → `/dashboard/keys`.
+- `sites/list`: added "Connect" link per site row.
+- 12 new tests (keys unified + tool access UI smoke).
+
+#### Changed
+- **Site-scoped tool access** (schema v7): dropped `user_tool_toggles` in favour of `site_tool_toggles(site_id)`; added `sites.tool_scope` column (default `admin`). Resolves the limitation that a user with multiple sites of the same plugin (e.g. Coolify production + staging) needed independent tool filters.
+- `ToolAccessManager` now takes `site_id`; `get_visible_tools` intersects key-scope categories with the site's `tool_scope` preset, then applies `site_tool_toggles` overrides.
+- Dashboard API routes moved under `/api/sites/{site_id}/...`.
+- `/dashboard/connect` → `/dashboard/keys` (301); `/dashboard/api-keys` → `/dashboard/keys` (301).
+
+---
+
+## [3.8.0] — 2026-04-07
+
+### Coolify plugin Phase 2+3 — projects, databases, services (Track F.17)
+
+#### Added
+- **Coolify projects handler** (8 tools): CRUD projects + environments.
+- **Coolify databases handler** (16 tools): CRUD, lifecycle, 6 DB types, backups.
+- **Coolify services handler** (13 tools): CRUD, lifecycle, env vars.
+- **Scope-based tool visibility + per-user toggles** (Track F.7 core): every `ToolDefinition` now carries `category` + `sensitivity` fields. Extends the scope model with `deploy` and `read:sensitive`. `tools/list` filters by the API key's scopes via `SCOPE_TO_CATEGORIES` in `core/tool_access.py`. Coolify handlers are annotated; other plugins keep the default `read` category (backward compatible).
+
+#### Changed
+- **Coolify plugin total: 67 tools** (was 30). Platform total: **633 tools**.
+- **766 tests passing** (48 new).
+
+---
+
+## [3.7.0] — 2026-04-04
+
+### Coolify MCP plugin — Phase 1 MVP (Track F.17)
+
+#### Added
+- **Coolify plugin** (30 tools, admin-only):
+ - **Applications** (17): CRUD, lifecycle (start/stop/restart), logs, env vars.
+ - **Deployments** (5): list, get, cancel, deploy by tag/UUID, app history.
+ - **Servers** (8): CRUD, resources, domains, validation.
+- Coolify credential fields added to `site_api` (URL + API token), with health check.
+- Registered in `server.py` startup so Coolify tools show up in `/coolify/mcp`.
+
+#### Changed
+- Platform plugin count: **10** (added Coolify). Tool count: **597**.
+
+---
+
## [3.6.0] — 2026-04-02
### Gitea Plugin — Public Release (Track F.16)
diff --git a/CLAUDE.md b/CLAUDE.md
index be9cedc..429fc1a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
-**MCP Hub** — a Python MCP (Model Context Protocol) server that manages multiple self-hosted services through a unified plugin architecture. Supports 10 plugin types (WordPress, WooCommerce, WordPress Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus, Coolify) with 597 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 10 plugin types (WordPress, WooCommerce, WordPress Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus, Coolify) with 633 tools total. The tool count stays constant regardless of how many sites are configured.
## Quick Setup
@@ -70,7 +70,7 @@ All configured in `pyproject.toml`:
```
├── server.py # Primary entry point
├── core/ # Layer 1: Core system modules
-├── plugins/ # Layer 2: Plugin system (9 plugins)
+├── plugins/ # Layer 2: Plugin system (10 plugins)
├── core/templates/ # Jinja2 templates (dashboard + OAuth)
├── tests/ # Organized test suite
├── scripts/ # Setup & deployment scripts
@@ -85,7 +85,7 @@ All configured in `pyproject.toml`:
```
Layer 1: Core System (core/) — Auth, site discovery, tool registry, health, rate limiting
-Layer 2: Plugin System (plugins/) — 9 plugin types, each with handlers + schemas
+Layer 2: Plugin System (plugins/) — 10 plugin types, each with handlers + schemas
Layer 3: API & Web UI (server.py + core/dashboard/) — FastMCP server, Starlette routes, dashboard
```
diff --git a/DOCKER_README.md b/DOCKER_README.md
index a3d5b81..479efb5 100644
--- a/DOCKER_README.md
+++ b/DOCKER_README.md
@@ -2,7 +2,7 @@
**The AI-native management hub for WordPress, WooCommerce, and self-hosted services.**
-565 tools across 9 plugins. Connect your sites, stores, repos, and databases — manage them all through Claude, ChatGPT, Cursor, or any MCP client.
+633 tools across 10 plugins (incl. Coolify). Connect your sites, stores, repos, and databases — manage them all through Claude, ChatGPT, Cursor, or any MCP client.
> **Don't want to self-host?** Try the hosted instance at **[mcp.palebluedot.live](https://mcp.palebluedot.live)** — log in with GitHub or Google, add your sites, and connect your AI client in minutes.
@@ -92,7 +92,7 @@ Use the most specific endpoint for your use case:
| `/u/{user_id}/{alias}/mcp` | 22-100 | No (pre-scoped) | Hosted/OAuth users |
| `/project/{alias}/mcp` | 22-100 | No (pre-scoped) | Single-site workflow |
| `/{plugin}/mcp` | 23-101 | Yes | Multi-site management |
-| `/mcp` | 565 | Yes | Admin & discovery only |
+| `/mcp` | 633 | 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`
diff --git a/README.md b/README.md
index f9de079..21c3df8 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,8 @@ Connect your sites, stores, repos, and databases — manage them all through Cla
[](https://www.python.org/)
[](https://pypi.org/project/mcphub-server/)
[](https://hub.docker.com/r/airano/mcphub)
-[]()
-[]()
+[]()
+[]()
[](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml)
@@ -49,21 +49,22 @@ MCP Hub is the first MCP server that lets you manage WordPress, WooCommerce, and
---
-## 565 Tools Across 9 Plugins
+## 633 Tools Across 10 Plugins
| Plugin | Tools | What You Can Do |
|--------|-------|-----------------|
| **WordPress** | 67 | Posts, pages, media, users, menus, taxonomies, SEO (Rank Math/Yoast) |
| **WooCommerce** | 28 | Products, orders, customers, coupons, reports, shipping |
| **WordPress Advanced** | 22 | Database ops, bulk operations, WP-CLI, system management |
-| **Gitea** | 56 | Repos, issues, pull requests, releases, webhooks, organizations |
+| **Gitea** | 58 | Repos, issues, pull requests, releases, webhooks, organizations, labels |
| **n8n** | 56 | Workflows, executions, credentials, variables, audit |
| **Supabase** | 70 | Database, auth, storage, edge functions, realtime |
| **OpenPanel** | 42 | Events, export, insights, profiles, projects, system |
| **Appwrite** | 100 | Databases, auth, storage, functions, teams, messaging |
| **Directus** | 100 | Collections, items, users, files, flows, permissions |
-| **System** | 24 | Health monitoring, API keys, OAuth management, audit |
-| **Total** | **565** | Constant count — scales to unlimited sites |
+| **Coolify** | 67 | Applications, deployments, servers, projects, databases, services |
+| **System** | 23 | Health monitoring, API keys, OAuth management, audit |
+| **Total** | **633** | Constant count — scales to unlimited sites |
---
@@ -265,29 +266,30 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
## Architecture
```
-/mcp → Admin endpoint (all 565 tools)
-/system/mcp → System tools only (24 tools)
+/mcp → Admin endpoint (all 633 tools)
+/system/mcp → System tools only (23 tools)
/wordpress/mcp → WordPress tools (67 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 (58 tools)
/n8n/mcp → n8n tools (56 tools)
/supabase/mcp → Supabase tools (70 tools)
/openpanel/mcp → OpenPanel tools (42 tools)
/appwrite/mcp → Appwrite tools (100 tools)
/directus/mcp → Directus tools (100 tools)
+/coolify/mcp → Coolify tools (67 tools)
/project/{alias}/mcp → Per-project endpoint (auto-injects site)
/u/{user_id}/{alias}/mcp → Per-user endpoint (hosted/OAuth users)
```
-**Recommendation**: Use plugin-specific endpoints instead of `/mcp` (565 tools) to minimize token usage.
+**Recommendation**: Use plugin-specific endpoints instead of `/mcp` (633 tools) to minimize token usage.
| Endpoint | Use Case | Tools |
|----------|----------|------:|
| `/u/{user_id}/{alias}/mcp` | Hosted users (OAuth login) | 22-100 |
| `/project/{alias}/mcp` | Single-site workflow (recommended) | 22-100 |
| `/{plugin}/mcp` | Multi-site management | 23-101 |
-| `/mcp` | Admin & discovery only | 565 |
+| `/mcp` | Admin & discovery only | 633 |
### Security
@@ -297,7 +299,7 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
- **GDPR-compliant audit logging** with automatic sensitive data filtering
- **Web dashboard** with real-time health monitoring (8 pages, EN/FA i18n)
-> **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 3.x (`>=3.0.0,<4.0.0`). The legacy multi-endpoint server and ProjectManager have been removed in v3.5.0.
### WordPress Plugin Requirements
diff --git a/core/dashboard/routes.py b/core/dashboard/routes.py
index 7ad1879..f4f2104 100644
--- a/core/dashboard/routes.py
+++ b/core/dashboard/routes.py
@@ -2913,16 +2913,22 @@ async def dashboard_connect_page(request: Request) -> Response:
async def dashboard_sites_view(request: Request) -> Response:
- """GET /dashboard/sites/{id} — Show site connect page with config snippets."""
+ """GET /dashboard/sites/{id} — Unified site management page (F.7c).
+
+ Combines connection settings, tool access, and connect/config snippets
+ into a single page with 3 sections.
+ """
user_session, redirect = _require_user_session(request)
if redirect:
return redirect
site_id = request.path_params.get("id", "")
+ import json
+
from core.config_snippets import get_supported_clients
- from core.site_api import PLUGIN_DISPLAY_NAMES as SITE_PLUGIN_NAMES
- from core.site_api import get_user_site
+ from core.site_api import get_user_credential_fields, get_user_plugin_names, get_user_site
+ from core.tool_access import get_scope_presets_for_plugin
site = await get_user_site(site_id, user_session["user_id"])
if site is None:
@@ -2936,15 +2942,23 @@ async def dashboard_sites_view(request: Request) -> Response:
public_url = os.environ.get("PUBLIC_URL", "http://localhost:8000").rstrip("/")
mcp_url = f"{public_url}/u/{user_session['user_id']}/{site['alias']}/mcp"
+ plugin_fields = get_user_credential_fields()
+ plugin_names = get_user_plugin_names()
+ scope_presets = get_scope_presets_for_plugin(site["plugin_type"])
+
return templates.TemplateResponse(
request,
- "dashboard/sites/view.html",
+ "dashboard/sites/manage.html",
{
"lang": lang,
"t": t,
"session": user_session,
"site": site,
- "plugin_names": SITE_PLUGIN_NAMES,
+ "plugin_names": plugin_names,
+ "plugin_fields": plugin_fields,
+ "plugin_fields_json": json.dumps(plugin_fields),
+ "scope_presets": scope_presets,
+ "scope_presets_json": json.dumps(scope_presets),
"mcp_url": mcp_url,
"clients": get_supported_clients(),
"current_page": "my_sites",
@@ -3009,7 +3023,9 @@ async def dashboard_keys_unified(request: Request) -> Response:
)
if user_session:
- # User view — personal keys
+ # User view — personal keys + config snippets
+ from core.config_snippets import get_supported_clients
+ from core.site_api import get_user_sites
from core.user_keys import get_user_key_manager
user_keys = []
@@ -3019,6 +3035,8 @@ async def dashboard_keys_unified(request: Request) -> Response:
except RuntimeError:
pass
+ sites = await get_user_sites(user_session["user_id"])
+
return templates.TemplateResponse(
request,
"dashboard/keys/list.html",
@@ -3028,6 +3046,8 @@ async def dashboard_keys_unified(request: Request) -> Response:
"session": user_session,
"is_admin": False,
"user_keys": user_keys,
+ "sites": sites,
+ "clients": get_supported_clients(),
"current_page": "keys",
},
)
@@ -3153,43 +3173,13 @@ async def api_test_site(request: Request) -> Response:
async def dashboard_sites_edit(request: Request) -> Response:
- """GET /dashboard/sites/{id}/edit — Render the Edit Site form."""
- user_session, redirect = _require_user_session(request)
- if redirect:
- return redirect
-
+ """GET /dashboard/sites/{id}/edit — Redirect to unified site page (F.7c)."""
site_id = request.path_params.get("id", "")
-
- from core.site_api import get_user_credential_fields, get_user_plugin_names, get_user_site
-
- site = await get_user_site(site_id, user_session["user_id"])
- if site is None:
- return RedirectResponse("/dashboard/sites?error=site_not_found", status_code=302)
-
- accept_language = request.headers.get("accept-language")
query_lang = request.query_params.get("lang")
- lang = detect_language(accept_language, query_lang)
- t = get_translations(lang)
-
- import json
-
- plugin_fields = get_user_credential_fields()
- plugin_names = get_user_plugin_names()
-
- return templates.TemplateResponse(
- request,
- "dashboard/sites/edit.html",
- {
- "lang": lang,
- "t": t,
- "session": user_session,
- "site": site,
- "plugin_fields": plugin_fields,
- "plugin_fields_json": json.dumps(plugin_fields),
- "plugin_names": plugin_names,
- "current_page": "my_sites",
- },
- )
+ url = f"/dashboard/sites/{site_id}"
+ if query_lang:
+ url += f"?lang={query_lang}"
+ return RedirectResponse(url, status_code=301)
async def api_update_site(request: Request) -> Response:
@@ -3243,11 +3233,25 @@ async def api_create_key(request: Request) -> Response:
try:
key_mgr = get_user_key_manager()
+
+ # Validate site_id if provided (must belong to user)
+ site_id = body.get("site_id")
+ if site_id:
+ from core.database import get_database
+
+ db = get_database()
+ site = await db.get_site(site_id, user_session["user_id"])
+ if site is None:
+ return JSONResponse({"error": "Site not found"}, status_code=404)
+
+ # F.7c: All user keys get full access — tool visibility is controlled
+ # per-site via tool_scope and per-tool toggles.
result = await key_mgr.create_key(
user_id=user_session["user_id"],
name=body.get("name", "Default"),
- scopes=body.get("scopes", "read write admin"),
+ scopes="read write admin",
expires_in_days=body.get("expires_in_days"),
+ site_id=site_id,
)
return JSONResponse({"key": result})
except RuntimeError as e:
@@ -3828,9 +3832,9 @@ def register_dashboard_routes(mcp):
mcp.custom_route("/dashboard/sites/add", methods=["GET"])(dashboard_sites_add)
mcp.custom_route("/dashboard/sites/{id}/edit", methods=["GET"])(dashboard_sites_edit)
mcp.custom_route("/dashboard/sites/{id}", methods=["GET"])(dashboard_sites_view)
- # /dashboard/connect → /dashboard/keys (301)
+ # /dashboard/connect → /dashboard/sites (F.7c: sites page has connect info now)
mcp.custom_route("/dashboard/connect", methods=["GET"])(
- lambda r: RedirectResponse("/dashboard/keys", status_code=301)
+ lambda r: RedirectResponse("/dashboard/sites", status_code=301)
)
# Service pages (F.3)
diff --git a/core/database.py b/core/database.py
index 69573c0..1ce3b89 100644
--- a/core/database.py
+++ b/core/database.py
@@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
_DEFAULT_DATA_DIR = "/app/data" if Path("/app").exists() else "./data"
# Schema version — increment when adding migrations
-SCHEMA_VERSION = 7
+SCHEMA_VERSION = 8
# Initial schema DDL
_SCHEMA_SQL = """\
@@ -83,7 +83,8 @@ CREATE TABLE IF NOT EXISTS user_api_keys (
last_used TEXT,
use_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
- expires_at TEXT
+ expires_at TEXT,
+ site_id TEXT
);
-- WP plugin connection tokens (short-lived, for MCP Connect plugin)
@@ -165,6 +166,10 @@ _MIGRATIONS: dict[int, str] = {
"ON site_tool_toggles(site_id);\n"
"ALTER TABLE sites ADD COLUMN tool_scope TEXT NOT NULL DEFAULT 'admin';\n"
),
+ 8: (
+ # F.7c: per-site API keys — allow keys scoped to a single site
+ "ALTER TABLE user_api_keys ADD COLUMN site_id TEXT;\n"
+ ),
}
@@ -682,6 +687,7 @@ class Database:
name: str,
scopes: str = "read write",
expires_at: str | None = None,
+ site_id: str | None = None,
) -> dict[str, Any]:
"""Create a new user API key.
@@ -692,6 +698,7 @@ class Database:
name: Human label (e.g. "Claude Desktop").
scopes: Space-separated scopes.
expires_at: Optional ISO 8601 expiry timestamp.
+ site_id: Optional site UUID to scope key to a single site.
Returns:
The created API key row as a dict.
@@ -701,14 +708,14 @@ class Database:
await self.execute(
"INSERT INTO user_api_keys "
- "(id, user_id, key_hash, key_prefix, name, scopes, created_at, expires_at) "
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
- (key_id, user_id, key_hash, key_prefix, name, scopes, now, expires_at),
+ "(id, user_id, key_hash, key_prefix, name, scopes, created_at, expires_at, site_id) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ (key_id, user_id, key_hash, key_prefix, name, scopes, now, expires_at, site_id),
)
result = await self.fetchone(
"SELECT id, user_id, key_prefix, name, scopes, last_used, use_count, "
- "created_at, expires_at FROM user_api_keys WHERE id = ?",
+ "created_at, expires_at, site_id FROM user_api_keys WHERE id = ?",
(key_id,),
)
if result is None:
@@ -726,7 +733,7 @@ class Database:
"""
return await self.fetchall(
"SELECT id, user_id, key_prefix, name, scopes, last_used, use_count, "
- "created_at, expires_at FROM user_api_keys WHERE user_id = ? ORDER BY created_at",
+ "created_at, expires_at, site_id FROM user_api_keys WHERE user_id = ? ORDER BY created_at",
(user_id,),
)
diff --git a/core/site_api.py b/core/site_api.py
index cf8c581..f1eb2a4 100644
--- a/core/site_api.py
+++ b/core/site_api.py
@@ -157,7 +157,7 @@ PLUGIN_CREDENTIAL_FIELDS: dict[str, list[dict[str, Any]]] = {
"label": "Client ID",
"type": "text",
"required": True,
- "hint": "OpenPanel Dashboard → Settings → Clients → Create client with 'root' mode for full access",
+ "hint": "Open your project on dashboard.openpanel.dev → Settings → Clients (URL: dashboard.openpanel.dev/{org}/{project-id}/settings/clients). Create a client in 'root' mode for full access.",
},
{
"name": "client_secret",
diff --git a/core/templates/dashboard/base.html b/core/templates/dashboard/base.html
index 8fcd15a..47bb74e 100644
--- a/core/templates/dashboard/base.html
+++ b/core/templates/dashboard/base.html
@@ -111,9 +111,6 @@
MCP Hub
-