diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2245027..6311780 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,38 @@ 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.4.0] — 2026-03-31
+
+### OpenPanel Plugin — Public Release (Track F.10)
+
+OpenPanel product analytics plugin fully reviewed, tested, and published for public use. Works with both self-hosted and cloud (openpanel.dev) instances.
+
+#### Added
+- **OpenPanel Plugin** (42 tools): Complete rewrite using public REST APIs — no tRPC/session dependency
+ - **Track API** (11 tools): track_event, page_view, screen_view, identify_user, set_user_properties, increment/decrement_property, create_group, assign_group, track_revenue, track_batch
+ - **Export API** (10 tools): export_events, export_events_csv, export_chart_data, get_event_count, get_unique_users, get_page_views, get_top_pages, get_top_referrers, get_geo_data, get_device_data
+ - **Insights API** (2 tools): get_overview_report, get_realtime_stats
+ - **Profile API** (3 tools): get_profile_events, get_profile_sessions, export_profile_data
+ - **Manage API** (10 tools): list/get/create/update/delete projects and clients
+ - **System** (6 tools): health_check, get_instance_info, get_usage_stats, get_storage_stats, test_connection, get_rate_limit_status
+- **Service Page descriptions**: Per-plugin description and notes section on service detail pages
+- **Dynamic URL hints**: Add Site form shows per-plugin URL guidance (e.g., OpenPanel API URL vs dashboard URL)
+- **WordPress plugin download**: OpenPanel WordPress plugin bundled at `/static/plugins/openpanel-self-hosted.zip`
+- **62 unit tests** for OpenPanel plugin (`tests/test_openpanel_plugin.py`)
+
+#### Changed
+- `ENABLED_PLUGINS` default: `wordpress,woocommerce,supabase,openpanel` (was: `wordpress,woocommerce,supabase`)
+- OpenPanel health check uses `GET /healthcheck` (was: non-existent `/api/v1/oauth/token`)
+- OpenPanel credential fields: added Project ID and Organization ID (optional)
+
+#### Removed
+- OpenPanel `alias_user` tool (explicitly unsupported by OpenPanel API)
+- OpenPanel dashboard handler (18 tools) — no public API available, was placeholder stubs
+- OpenPanel funnel handler (8 tools) — no public API available, was placeholder stubs
+- tRPC client dependency — all tools now use public REST APIs
+
+---
+
## [3.3.0] — 2026-03-31
### Platform Hardening & Admin Unification (Track F.1–F.8)
@@ -22,7 +54,7 @@ Major quality release: plugin visibility control, UI/UX polish, unified admin pa
- **Pre-configured OAuth** (F.2): Default OAuth redirect URIs for Claude.ai; green tip about optional OAuth
#### Fixed
-- **Connect Page** (F.2): WordPress/SEO amber info box now shown only for WordPress/WooCommerce sites (was shown for all plugin types)
+- **Connect Page** (F.2): WordPress/SEO amber info box shown only for WordPress/WooCommerce sites (was shown for all plugin types)
- **Sidebar Version** (F.2): Fixed "v" displaying when version string is empty
- **Auth Page Language** (F.2): Auth page defaults to English regardless of Accept-Language header
- **Donation Link** (F.2): Moved from home page to sidebar for consistent visibility
diff --git a/CLAUDE.md b/CLAUDE.md
index 8e34f9e..b7cd90f 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 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.
+**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 565 tools total. The tool count stays constant regardless of how many sites are configured.
## Quick Setup
@@ -193,15 +193,15 @@ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
## Live Instances
-- **Platform**: `mcp.palebluedot.live` — Live MCP Hub with OAuth login
-- **Blog**: `blog.palebluedot.live` — WordPress test site + project blog
+- **Platform**: `mcp.example.com` — Live MCP Hub with OAuth login
+- **Blog**: `blog.example.com` — WordPress test site + project blog
- **Deployment**: Coolify / Docker Compose, port 8000
## Current Development (Track F)
v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claude/plans/structured-popping-adleman.md` for full plan.
-**Active plugins for public users**: WordPress, WooCommerce, Supabase (configurable via `ENABLED_PLUGINS` env var)
+**Active plugins for public users**: WordPress, WooCommerce, Supabase, OpenPanel (configurable via `ENABLED_PLUGINS` env var)
## Gotchas
@@ -209,7 +209,7 @@ v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claud
- `server_multi.py` is the alternative multi-endpoint entry point; `server.py` is the primary
- `wordpress-plugin/` contains companion WP plugins (openpanel, airano-mcp-seo-bridge) — these are PHP, not Python
- `env.example` has "FUTURE" labels for Supabase/Gitea but both are fully implemented
-- Only 3 plugins are tested for public use: WordPress, WooCommerce, Supabase. Others are admin-only or disabled.
+- 4 plugins are tested for public use: WordPress, WooCommerce, Supabase, OpenPanel. Others are admin-only or disabled.
- OAuth Clients (Client ID/Secret) are for MCP endpoint auth, NOT the same as GitHub/Google dashboard login
- User sites stored in SQLite (`core/database.py`), admin sites still from env vars
- Dashboard templates live in `core/templates/` (included in pip package as `package_data`)
@@ -225,4 +225,4 @@ v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claud
- Required env vars: `MASTER_API_KEY`, `OAUTH_JWT_SECRET_KEY`, `OAUTH_BASE_URL`
- OAuth env vars: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `PUBLIC_URL`
- User limits: `MAX_SITES_PER_USER=10`, `USER_RATE_LIMIT_PER_MIN=30`, `USER_RATE_LIMIT_PER_HR=500`
-- Plugin visibility: `ENABLED_PLUGINS=wordpress,woocommerce,supabase` (default)
+- Plugin visibility: `ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel` (default)
diff --git a/README.md b/README.md
index 873e13f..44b4658 100644
--- a/README.md
+++ b/README.md
@@ -112,14 +112,14 @@ You should see the login page. Log in with your `MASTER_API_KEY` or via **GitHub
### Try It Now (No Setup Required)
-**Don't want to self-host?** Try the demo instance at **[mcp.palebluedot.live](https://mcp.palebluedot.live)**:
+**Don't want to self-host?** Use the hosted instance at **[mcp.example.com](https://mcp.example.com)**:
1. Log in with **GitHub** or **Google**
2. Add your sites via the dashboard (My Sites → Add Service)
3. Go to **Connect** page — generate config for your AI client
4. Copy-paste the config into Claude Desktop, VS Code, or Claude Code
-Your personal MCP endpoint: `https://mcp.palebluedot.live/u/{your-user-id}/{alias}/mcp`
+Your personal MCP endpoint: `https://mcp.example.com/u/{your-user-id}/{alias}/mcp`
---
diff --git a/core/api_keys.py b/core/api_keys.py
index 62abd6a..24c04f9 100644
--- a/core/api_keys.py
+++ b/core/api_keys.py
@@ -10,12 +10,12 @@ import json
import logging
import os
import secrets
+
+import bcrypt
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta
from pathlib import Path
-import bcrypt
-
logger = logging.getLogger(__name__)
diff --git a/core/dashboard/routes.py b/core/dashboard/routes.py
index ddf9f45..bb0dbc0 100644
--- a/core/dashboard/routes.py
+++ b/core/dashboard/routes.py
@@ -850,12 +850,9 @@ async def get_all_projects(
is_admin = False
current_user_id = None
if user_session:
- if (
- hasattr(user_session, "user_type")
- and user_session.user_type == "master"
- or isinstance(user_session, dict)
- and user_session.get("role") == "admin"
- ):
+ if hasattr(user_session, "user_type") and user_session.user_type == "master":
+ is_admin = True
+ elif isinstance(user_session, dict) and user_session.get("role") == "admin":
is_admin = True
elif isinstance(user_session, dict) and "user_id" in user_session:
current_user_id = user_session["user_id"]
@@ -3292,6 +3289,46 @@ async def get_service_page_data(plugin_type: str) -> dict | None:
from core.plugin_visibility import is_plugin_public
+ # Per-plugin descriptions and notes
+ service_descriptions = {
+ "openpanel": {
+ "en": (
+ "OpenPanel product analytics management. "
+ "Supports event tracking, data export, analytics reports, "
+ "and project/client management via public REST APIs. "
+ "Works with both self-hosted and cloud (openpanel.dev) instances."
+ ),
+ "fa": (
+ "مدیریت آنالیتیکس محصول OpenPanel. "
+ "پشتیبانی از ردیابی رویداد، خروجی داده، گزارشهای آنالیتیکس، "
+ "و مدیریت پروژه/کلاینت از طریق APIهای عمومی REST. "
+ "هم نسخه خودمیزبان و هم نسخه cloud (openpanel.dev) پشتیبانی میشود."
+ ),
+ "notes_en": [
+ "Site URL must be the API URL, not the dashboard URL. "
+ "Cloud: https://api.openpanel.dev — "
+ "Self-hosted: your API service URL (e.g., https://analytics.example.com).",
+ "Client must have root mode for full access (tracking + export + manage). "
+ "Use read mode for analytics only, or write mode for tracking only.",
+ "If you have WordPress, install the OpenPanel WordPress plugin "
+ "to automatically send analytics data to your OpenPanel instance.",
+ ],
+ "notes_fa": [
+ "آدرس سایت باید آدرس API باشد، نه داشبورد. "
+ "نسخه Cloud: https://api.openpanel.dev — "
+ "خودمیزبان: آدرس سرویس API شما (مثلاً https://analytics.example.com).",
+ "برای دسترسی کامل، کلاینت باید حالت root داشته باشد. "
+ "از حالت read برای آنالیتیکس و write برای ردیابی استفاده کنید.",
+ "اگر وردپرس دارید، افزونه OpenPanel WordPress "
+ "را نصب کنید تا دادههای آنالیتیکس بهصورت خودکار به OpenPanel ارسال شود.",
+ ],
+ },
+ }
+
+ desc_data = service_descriptions.get(plugin_type, {})
+
return {
"plugin_type": plugin_type,
"display_name": display_name,
@@ -3299,6 +3336,7 @@ async def get_service_page_data(plugin_type: str) -> dict | None:
"tools_count": len(tools),
"credential_fields": credential_fields,
"is_public": is_plugin_public(plugin_type),
+ "description": desc_data,
}
diff --git a/core/endpoints/config.py b/core/endpoints/config.py
index b22db17..c479206 100644
--- a/core/endpoints/config.py
+++ b/core/endpoints/config.py
@@ -249,16 +249,15 @@ ENDPOINT_CONFIGS = {
},
max_tools=80,
),
- # OpenPanel endpoint - Product Analytics (73 tools) - Phase H
+ # OpenPanel endpoint - Product Analytics (42 tools) - Phase H
EndpointType.OPENPANEL: EndpointConfig(
path="/openpanel",
name="OpenPanel Analytics",
- description="OpenPanel product analytics management (events, export, funnels, dashboards)",
+ description="OpenPanel self-hosted product analytics (events, export, insights, project/client management)",
endpoint_type=EndpointType.OPENPANEL,
plugin_types=["openpanel"],
require_master_key=False,
allowed_scopes={"read", "write", "admin"},
- # Blacklist system and admin tools
tool_blacklist={
"manage_api_keys_create",
"manage_api_keys_delete",
@@ -266,7 +265,7 @@ ENDPOINT_CONFIGS = {
"oauth_register_client",
"oauth_revoke_client",
},
- max_tools=80,
+ max_tools=50,
),
# Appwrite endpoint - Backend-as-a-Service (100 tools) - Phase I
EndpointType.APPWRITE: EndpointConfig(
diff --git a/core/plugin_visibility.py b/core/plugin_visibility.py
index e79add5..0287a06 100644
--- a/core/plugin_visibility.py
+++ b/core/plugin_visibility.py
@@ -15,7 +15,7 @@ Usage:
import os
# Default plugins available to public (OAuth) users
-DEFAULT_PUBLIC_PLUGINS = {"wordpress", "woocommerce", "supabase"}
+DEFAULT_PUBLIC_PLUGINS = {"wordpress", "woocommerce", "supabase", "openpanel"}
def _parse_plugins(val: str) -> set[str]:
diff --git a/core/settings.py b/core/settings.py
index 03ab3fc..160cea8 100644
--- a/core/settings.py
+++ b/core/settings.py
@@ -3,7 +3,7 @@
Usage:
from core.settings import get_setting
- enabled = await get_setting("ENABLED_PLUGINS", "wordpress,woocommerce,supabase")
+ enabled = await get_setting("ENABLED_PLUGINS", "wordpress,woocommerce,supabase,openpanel")
max_sites = int(await get_setting("MAX_SITES_PER_USER", "10"))
"""
@@ -17,7 +17,7 @@ _cached_plugins: set[str] | None = None
# Default values for all managed settings
SETTING_DEFAULTS: dict[str, str] = {
- "ENABLED_PLUGINS": "wordpress,woocommerce,supabase",
+ "ENABLED_PLUGINS": "wordpress,woocommerce,supabase,openpanel",
"MAX_SITES_PER_USER": "10",
"USER_RATE_LIMIT_PER_MIN": "30",
"USER_RATE_LIMIT_PER_HR": "500",
diff --git a/core/site_api.py b/core/site_api.py
index 6447025..a4b5586 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 admin panel → API section",
+ "hint": "OpenPanel Dashboard → Settings → Clients → Create client with 'root' mode for full access",
},
{
"name": "client_secret",
@@ -166,6 +166,21 @@ PLUGIN_CREDENTIAL_FIELDS: dict[str, list[dict[str, Any]]] = {
"required": True,
"hint": "Generated with your Client ID",
},
+ {
+ "name": "project_id",
+ "label": "Project ID",
+ "type": "text",
+ "required": False,
+ "hint": "From dashboard URL: dashboard.openpanel.dev/{org}/{project-id}/ — sets default for Export & Insights tools",
+ },
+ {
+ "name": "organization_id",
+ "label": "Organization ID",
+ "type": "text",
+ "required": False,
+ "hint": "From dashboard URL: dashboard.openpanel.dev/{org}/{project-id}/",
+ "advanced": True,
+ },
],
"appwrite": [
{
@@ -215,7 +230,7 @@ _HEALTH_ENDPOINTS: dict[str, dict[str, Any]] = {
"gitea": {"path": "/api/v1/user", "method": "GET"},
"n8n": {"path": "/healthz", "method": "GET"},
"supabase": {"path": "/rest/v1/", "method": "GET"},
- "openpanel": {"path": "/api/v1/oauth/token", "method": "POST"},
+ "openpanel": {"path": "/healthcheck", "method": "GET"},
"appwrite": {"path": "/v1/health", "method": "GET"},
"directus": {"path": "/server/health", "method": "GET"},
}
@@ -342,23 +357,13 @@ async def validate_site_connection(
elif plugin_type == "directus":
headers["Authorization"] = f"Bearer {credentials.get('token', '')}"
elif plugin_type == "openpanel":
- # OpenPanel uses token exchange — just check that the URL is reachable
- pass
+ headers["openpanel-client-id"] = credentials.get("client_id", "")
+ headers["openpanel-client-secret"] = credentials.get("client_secret", "")
try:
timeout = aiohttp.ClientTimeout(total=15)
async with aiohttp.ClientSession(timeout=timeout) as session:
- if method == "POST" and plugin_type == "openpanel":
- async with session.post(
- check_url,
- json={
- "clientId": credentials.get("client_id", ""),
- "clientSecret": credentials.get("client_secret", ""),
- },
- ) as resp:
- status_code = resp.status
- resp_text = await resp.text()
- elif method == "POST":
+ if method == "POST":
async with session.post(check_url, headers=headers) as resp:
status_code = resp.status
resp_text = await resp.text()
diff --git a/core/templates/dashboard/service.html b/core/templates/dashboard/service.html
index 6c85a75..b241e41 100644
--- a/core/templates/dashboard/service.html
+++ b/core/templates/dashboard/service.html
@@ -31,6 +31,31 @@
+
+ {% if service.description %}
+
{{ service.description.fa }}
+ {% elif service.description.en %} +{{ service.description.en }}
+ {% endif %} + + {% set notes_key = 'notes_fa' if lang == 'fa' else 'notes_en' %} + {% if service.description[notes_key] %} +