4 Commits

Author SHA1 Message Date
bf893a743a fix(F.16): update visibility tests — gitea is now public
Some checks failed
Release / Test before release (push) Has been cancelled
Release / Publish to PyPI (push) Has been cancelled
Release / Publish to Docker Hub (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 08:00:16 +02:00
2c9d4ed3d2 feat(F.16): Gitea plugin review, test & public release — v3.6.0
- Added update_webhook and delete_label tools (58 total Gitea tools)
- 97 unit tests (test_gitea_plugin.py)
- Gitea now public for all users (DEFAULT_PUBLIC_PLUGINS)
- Fixed merge_pull_request optional field handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 07:45:17 +02:00
4f0c2f9bb0 chore: add glama.json for MCP server registry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 06:13:50 +02:00
d79c2fd94a style: fix black formatting in routes.py
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 06:08:56 +02:00
14 changed files with 1446 additions and 23 deletions

View File

@@ -5,6 +5,24 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.6.0] — 2026-04-02
### Gitea Plugin — Public Release (Track F.16)
Gitea plugin fully reviewed, tested, and published for public use. Two missing tools added, comprehensive test suite created.
#### Added
- **`update_webhook` tool**: Update existing webhook configuration, events, and active status (admin scope)
- **`delete_label` tool**: Delete labels from repositories (write scope)
- **`tests/test_gitea_plugin.py`**: Comprehensive test suite — 85+ tests covering client init, headers, tool specs, all 5 handler groups, health check, and plugin delegation
#### Changed
- **Gitea plugin now public**: Added `gitea` to `DEFAULT_PUBLIC_PLUGINS` — available to all OAuth users
- **Tool count**: 565 → 567 (added update_webhook + delete_label)
- **env.example**: Updated default `ENABLED_PLUGINS` to include `gitea`
---
## [3.5.0] — 2026-04-02 ## [3.5.0] — 2026-04-02
### FastMCP Upgrade & Legacy Cleanup (Track F.15) ### FastMCP Upgrade & Legacy Cleanup (Track F.15)

View File

@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
**MCP Hub** — a Python MCP (Model Context Protocol) server that manages multiple self-hosted 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. **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 567 tools total. The tool count stays constant regardless of how many sites are configured.
## Quick Setup ## Quick Setup
@@ -185,14 +185,14 @@ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claude/plans/structured-popping-adleman.md` for full plan. 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, OpenPanel (configurable via `ENABLED_PLUGINS` env var) **Active plugins for public users**: WordPress, WooCommerce, Supabase, OpenPanel, Gitea (configurable via `ENABLED_PLUGINS` env var)
## Gotchas ## Gotchas
- Test files exist in both `tests/` (proper) and root directory (legacy `test_*.py`). Run `pytest tests/` for organized tests only. - Test files exist in both `tests/` (proper) and root directory (legacy `test_*.py`). Run `pytest tests/` for organized tests only.
- `wordpress-plugin/` contains companion WP plugins (openpanel, airano-mcp-seo-bridge) — these are PHP, not Python - `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 - `env.example` has "FUTURE" labels for Supabase/Gitea but both are fully implemented
- 4 plugins are tested for public use: WordPress, WooCommerce, Supabase, OpenPanel. Others are admin-only or disabled. - 5 plugins are tested for public use: WordPress, WooCommerce, Supabase, OpenPanel, Gitea. Others are admin-only or disabled.
- OAuth Clients (Client ID/Secret) are for MCP endpoint auth, NOT the same as GitHub/Google dashboard login - OAuth Clients (Client ID/Secret) are for MCP endpoint auth, NOT the same as GitHub/Google dashboard login
- All sites stored in SQLite (`core/database.py`), managed via web dashboard - All sites stored in SQLite (`core/database.py`), managed via web dashboard
- Dashboard templates live in `core/templates/` (included in pip package as `package_data`) - Dashboard templates live in `core/templates/` (included in pip package as `package_data`)
@@ -208,4 +208,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` - 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` - 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` - 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,openpanel` (default) - Plugin visibility: `ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel,gitea` (default)

View File

@@ -850,7 +850,12 @@ async def get_all_projects(
is_admin = False is_admin = False
current_user_id = None current_user_id = None
if user_session: 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"
or isinstance(user_session, dict)
and user_session.get("role") == "admin"
):
is_admin = True is_admin = True
elif isinstance(user_session, dict) and "user_id" in user_session: elif isinstance(user_session, dict) and "user_id" in user_session:
current_user_id = user_session["user_id"] current_user_id = user_session["user_id"]

View File

@@ -8,14 +8,14 @@ Usage:
from core.plugin_visibility import get_public_plugin_types, is_plugin_public from core.plugin_visibility import get_public_plugin_types, is_plugin_public
public_types = get_public_plugin_types() # {"wordpress", "woocommerce", "supabase"} public_types = get_public_plugin_types() # {"wordpress", "woocommerce", "supabase"}
if is_plugin_public("gitea"): # False if is_plugin_public("gitea"): # True
... ...
""" """
import os import os
# Default plugins available to public (OAuth) users # Default plugins available to public (OAuth) users
DEFAULT_PUBLIC_PLUGINS = {"wordpress", "woocommerce", "supabase", "openpanel"} DEFAULT_PUBLIC_PLUGINS = {"wordpress", "woocommerce", "supabase", "openpanel", "gitea"}
def _parse_plugins(val: str) -> set[str]: def _parse_plugins(val: str) -> set[str]:

View File

@@ -3,7 +3,7 @@
Usage: Usage:
from core.settings import get_setting from core.settings import get_setting
enabled = await get_setting("ENABLED_PLUGINS", "wordpress,woocommerce,supabase,openpanel") enabled = await get_setting("ENABLED_PLUGINS", "wordpress,woocommerce,supabase,openpanel,gitea")
max_sites = int(await get_setting("MAX_SITES_PER_USER", "10")) 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 # Default values for all managed settings
SETTING_DEFAULTS: dict[str, str] = { SETTING_DEFAULTS: dict[str, str] = {
"ENABLED_PLUGINS": "wordpress,woocommerce,supabase,openpanel", "ENABLED_PLUGINS": "wordpress,woocommerce,supabase,openpanel,gitea",
"MAX_SITES_PER_USER": "10", "MAX_SITES_PER_USER": "10",
"USER_RATE_LIMIT_PER_MIN": "30", "USER_RATE_LIMIT_PER_MIN": "30",
"USER_RATE_LIMIT_PER_HR": "500", "USER_RATE_LIMIT_PER_HR": "500",

View File

@@ -0,0 +1,90 @@
# F.16 — Gitea Plugin Review, Test & Public Release
> Session prompt. Copy and paste into a new Claude Code conversation.
---
## Prompt:
```
از مهارت project-ops برای آشنایی با محیط استفاده کن. پروژه mcphub-internal روی branch Phase-1 است.
فایل‌های مرجع:
- docs/plans/2026-03-25-v4-development-cycle.md
- CLAUDE.md
- CHANGELOG.md
## وضعیت فعلی
- نسخه: v3.5.0
- 565 ابزار در 9 پلاگین (4 پلاگین عمومی: WordPress, WooCommerce, Supabase, OpenPanel)
- FastMCP: `>=3.0.0,<4.0.0`
- CI سبز
- F.15 تکمیل شده — FastMCP 3.x upgrade + legacy cleanup
## هدف: فاز F.16 — Gitea Plugin Review, Test & Public Enablement
مشابه F.10 (OpenPanel) — بررسی، تست و فعال‌سازی عمومی پلاگین Gitea.
### بخش 1: بررسی و تحلیل
1. **ساختار پلاگین**: `plugins/gitea/` شامل 56 ابزار در 5 هندلر:
- `handlers/repositories.py` — 16 tools (CRUD repos, branches, tags, files)
- `handlers/issues.py` — 12 tools (issues, comments, labels, milestones)
- `handlers/pull_requests.py` — 15 tools (PRs, reviews, merge, diff)
- `handlers/users.py` — 8 tools (users, orgs, teams)
- `handlers/webhooks.py` — 5 tools (CRUD webhooks, test)
2. **مشکلات شناسایی‌شده**:
- `update_webhook` در client.py هست ولی به عنوان tool expose نشده
- هیچ تست اختصاصی‌ای ندارد (0 تست!)
- فعلاً admin-only هست (نه در ENABLED_PLUGINS)
3. **تست واقعی**: سایت Gitea از MCPHub MCP endpoint:
- از ابزارهای Supabase MCP (`mcp.example.com`) استفاده کنید تا سایت Gitea موجود را پیدا کنید
- یا یک Gitea instance جدید از طریق داشبورد اضافه کنید
- هر دسته tool را با API واقعی تست کنید
### بخش 2: تست نوشتن
مشابه `tests/test_openpanel_plugin.py` (الگو — 767 خط, 62 تست):
1. `tests/test_gitea_plugin.py` بنویسید:
- Client initialization + health check
- Tool spec validation (نام‌ها، پارامترها، type ها)
- Handler delegation (mock client)
- Error handling
- Pagination
2. هدف: حداقل 80 تست (56 tool + edge cases)
### بخش 3: رفع مشکلات
1. `update_webhook` را به عنوان tool expose کنید
2. هر tool که در تست واقعی مشکل دارد را fix کنید
3. توضیحات service page برای Gitea بنویسید
4. health check endpoint را بررسی کنید
### بخش 4: فعال‌سازی عمومی
1. `core/plugin_visibility.py`: اضافه کردن `gitea` به `DEFAULT_PUBLIC_PLUGINS`
2. `env.example`: آپدیت ENABLED_PLUGINS default
3. `glama.json`: آپدیت description اگر لازمه
### بخش 5: Release
1. Version bump: `3.5.0` → `3.6.0`
2. CHANGELOG update
3. Lint: `uvx --python 3.12 black .` && `uvx ruff check --fix .`
4. Commit + push Phase-1
5. Sync: `python3.11 scripts/community-build/sync.py --output ../mcphub/`
6. بعد از sync حتماً `uvx --python 3.12 black .` در public repo هم بزنید!
7. Commit + push public repo
### مراحل پیشنهادی
1. **تحلیل**: خواندن کامل هر handler + client + schemas
2. **تست واقعی**: اتصال به Gitea instance و تست ابزارها
3. **پلن اجرایی**: ارائه پلن و تایید قبل از شروع
4. **تست نوشتن**: test_gitea_plugin.py
5. **رفع مشکلات**: fix هر tool مشکل‌دار
6. **فعال‌سازی**: ENABLED_PLUGINS update
7. **Release**: v3.6.0
### نکات فنی
- Private repo: /config/workspace/mcphub-internal (branch Phase-1)
- Public repo: /config/workspace/mcphub (branch main)
- Lint: `uvx --python 3.12 black .` && `uvx ruff check --fix .`
- Sync: `python3.11 scripts/community-build/sync.py --output ../mcphub/`
- ایمیل git عمومی: hi.airano@gmail.com
- **مهم**: اول پلن بده، بدون تایید شروع نکن
- **الگوی مرجع**: F.10 (OpenPanel) — بخش "Phase F.10" در plan و `tests/test_openpanel_plugin.py`
```

View File

@@ -32,8 +32,8 @@ DASHBOARD_SESSION_SECRET=
# ============================================ # ============================================
# Comma-separated list of plugins visible to public (OAuth) users. # Comma-separated list of plugins visible to public (OAuth) users.
# Admin users always see all plugins regardless of this setting. # Admin users always see all plugins regardless of this setting.
# Default (if not set): wordpress,woocommerce,supabase,openpanel # Default (if not set): wordpress,woocommerce,supabase,openpanel,gitea
# ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel # ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel,gitea
# ============================================ # ============================================
# ADMIN SYSTEM (Track F.4) # ADMIN SYSTEM (Track F.4)

36
glama.json Normal file
View File

@@ -0,0 +1,36 @@
{
"$schema": "https://glama.ai/mcp/schemas/server.json",
"maintainers": ["airano-ir"],
"name": "mcphub",
"display_name": "MCP Hub",
"description": "Unified MCP server for managing WordPress, WooCommerce, Gitea, n8n, Supabase, OpenPanel, Appwrite, and Directus with 567 tools, multi-site support, and OAuth 2.1 authentication.",
"repository": "https://github.com/airano-ir/mcphub",
"homepage": "https://mcp.palebluedot.live",
"version": "3.6.0",
"license": "MIT",
"runtime": "python",
"transport": ["stdio", "streamable-http"],
"categories": ["developer-tools", "cms", "databases", "automation"],
"keywords": ["mcp", "wordpress", "woocommerce", "supabase", "gitea", "n8n", "openpanel", "appwrite", "directus", "self-hosted", "multi-site"],
"author": {
"name": "airano-ir",
"url": "https://github.com/airano-ir"
},
"capabilities": {
"tools": true,
"prompts": false,
"resources": false
},
"relatedServers": [
{
"name": "WordPress MCP",
"url": "https://glama.ai/mcp/servers/developer-in-world/wordpress-mcp",
"description": "WordPress REST API MCP server"
},
{
"name": "Supabase MCP",
"url": "https://glama.ai/mcp/servers/supabase-community/supabase-mcp",
"description": "Official Supabase MCP server"
}
]
}

View File

@@ -295,6 +295,25 @@ def get_tool_specifications() -> list[dict[str, Any]]:
}, },
"scope": "write", "scope": "write",
}, },
{
"name": "delete_label",
"method_name": "delete_label",
"description": "Delete a label from a repository.",
"schema": {
"type": "object",
"properties": {
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
"label_id": {
"type": "integer",
"description": "Label ID to delete",
"minimum": 1,
},
},
"required": ["owner", "repo", "label_id"],
},
"scope": "write",
},
# === MILESTONES === # === MILESTONES ===
{ {
"name": "list_milestones", "name": "list_milestones",
@@ -499,6 +518,13 @@ async def create_label(
return json.dumps(result, indent=2) return json.dumps(result, indent=2)
async def delete_label(client: GiteaClient, owner: str, repo: str, label_id: int) -> str:
"""Delete a label"""
await client.delete_label(owner, repo, label_id)
result = {"success": True, "message": f"Label {label_id} deleted successfully"}
return json.dumps(result, indent=2)
# Milestone operations # Milestone operations
async def list_milestones( async def list_milestones(
client: GiteaClient, owner: str, repo: str, state: str | None = None client: GiteaClient, owner: str, repo: str, state: str | None = None

View File

@@ -526,12 +526,13 @@ async def merge_pull_request(
delete_branch_after_merge: bool = False, delete_branch_after_merge: bool = False,
) -> str: ) -> str:
"""Merge a pull request""" """Merge a pull request"""
data = { data: dict = {"Do": method}
"Do": method, if title is not None:
"MergeTitleField": title, data["MergeTitleField"] = title
"MergeMessageField": message, if message is not None:
"delete_branch_after_merge": delete_branch_after_merge, data["MergeMessageField"] = message
} if delete_branch_after_merge:
data["delete_branch_after_merge"] = True
merge_result = await client.merge_pull_request(owner, repo, pr_number, data) merge_result = await client.merge_pull_request(owner, repo, pr_number, data)
result = { result = {
"success": True, "success": True,

View File

@@ -159,6 +159,79 @@ def get_tool_specifications() -> list[dict[str, Any]]:
}, },
"scope": "read", "scope": "read",
}, },
{
"name": "update_webhook",
"method_name": "update_webhook",
"description": "Update an existing webhook's configuration, events, or active status.",
"schema": {
"type": "object",
"properties": {
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
"webhook_id": {
"type": "integer",
"description": "Webhook ID to update",
"minimum": 1,
},
"url": {
"anyOf": [{"type": "string", "format": "uri"}, {"type": "null"}],
"description": "New webhook URL",
},
"events": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"enum": [
"create",
"delete",
"fork",
"push",
"issues",
"issue_assign",
"issue_label",
"issue_milestone",
"issue_comment",
"pull_request",
"pull_request_assign",
"pull_request_label",
"pull_request_milestone",
"pull_request_comment",
"pull_request_review_approved",
"pull_request_review_rejected",
"pull_request_review_comment",
"pull_request_sync",
"wiki",
"repository",
"release",
],
},
},
{"type": "null"},
],
"description": "Updated list of events to trigger webhook",
},
"content_type": {
"anyOf": [
{"type": "string", "enum": ["json", "form"]},
{"type": "null"},
],
"description": "Content type for webhook payload",
},
"secret": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Secret for webhook signature verification",
},
"active": {
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"description": "Activate or deactivate webhook",
},
},
"required": ["owner", "repo", "webhook_id"],
},
"scope": "admin",
},
] ]
@@ -220,3 +293,40 @@ async def get_webhook(client: GiteaClient, owner: str, repo: str, webhook_id: in
webhook = await client.get_webhook(owner, repo, webhook_id) webhook = await client.get_webhook(owner, repo, webhook_id)
result = {"success": True, "webhook": webhook} result = {"success": True, "webhook": webhook}
return json.dumps(result, indent=2) return json.dumps(result, indent=2)
async def update_webhook(
client: GiteaClient,
owner: str,
repo: str,
webhook_id: int,
url: str | None = None,
events: list[str] | None = None,
content_type: str | None = None,
secret: str | None = None,
active: bool | None = None,
) -> str:
"""Update a webhook"""
data: dict = {}
if events is not None:
data["events"] = events
if active is not None:
data["active"] = active
config: dict = {}
if url is not None:
config["url"] = url
if content_type is not None:
config["content_type"] = content_type
if secret is not None:
config["secret"] = secret
if config:
data["config"] = config
webhook = await client.update_webhook(owner, repo, webhook_id, data)
result = {
"success": True,
"message": f"Webhook {webhook_id} updated successfully",
"webhook": webhook,
}
return json.dumps(result, indent=2)

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "mcphub-server" name = "mcphub-server"
version = "3.5.0" version = "3.6.0"
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"}

1135
tests/test_gitea_plugin.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -78,11 +78,11 @@ class TestIsPluginPublic:
os.environ.pop("ENABLED_PLUGINS", None) os.environ.pop("ENABLED_PLUGINS", None)
assert is_plugin_public("supabase") is True assert is_plugin_public("supabase") is True
def test_default_gitea_not_public(self): def test_default_gitea_public(self):
"""Gitea is not public by default.""" """Gitea is public by default (since F.16)."""
with patch.dict(os.environ, {}, clear=False): with patch.dict(os.environ, {}, clear=False):
os.environ.pop("ENABLED_PLUGINS", None) os.environ.pop("ENABLED_PLUGINS", None)
assert is_plugin_public("gitea") is False assert is_plugin_public("gitea") is True
def test_default_wordpress_advanced_not_public(self): def test_default_wordpress_advanced_not_public(self):
"""WordPress Advanced is not public by default.""" """WordPress Advanced is not public by default."""
@@ -118,8 +118,9 @@ class TestSiteApiIntegration:
assert "wordpress" in fields assert "wordpress" in fields
assert "woocommerce" in fields assert "woocommerce" in fields
assert "supabase" in fields assert "supabase" in fields
assert "gitea" in fields
assert "openpanel" in fields
assert "wordpress_advanced" not in fields assert "wordpress_advanced" not in fields
assert "gitea" not in fields
assert "n8n" not in fields assert "n8n" not in fields
def test_get_user_plugin_names_filtered(self): def test_get_user_plugin_names_filtered(self):
@@ -133,8 +134,9 @@ class TestSiteApiIntegration:
assert "wordpress" in names assert "wordpress" in names
assert "woocommerce" in names assert "woocommerce" in names
assert "supabase" in names assert "supabase" in names
assert "gitea" in names
assert "openpanel" in names
assert "wordpress_advanced" not in names assert "wordpress_advanced" not in names
assert "gitea" not in names
def test_custom_env_changes_fields(self): def test_custom_env_changes_fields(self):
"""Custom ENABLED_PLUGINS changes which credential fields are returned.""" """Custom ENABLED_PLUGINS changes which credential fields are returned."""