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 %} +
+ {% if lang == 'fa' and service.description.fa %} +

{{ 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] %} + + {% endif %} +
+ {% endif %} +
diff --git a/core/templates/dashboard/sites/add.html b/core/templates/dashboard/sites/add.html index 3bbb386..c5317d1 100644 --- a/core/templates/dashboard/sites/add.html +++ b/core/templates/dashboard/sites/add.html @@ -37,6 +37,7 @@ +
@@ -73,9 +74,23 @@