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/),
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
### 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
**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
@@ -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.
**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
- 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
- `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
- 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`)
@@ -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`
- 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,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
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"
or 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"]

View File

@@ -8,14 +8,14 @@ Usage:
from core.plugin_visibility import get_public_plugin_types, is_plugin_public
public_types = get_public_plugin_types() # {"wordpress", "woocommerce", "supabase"}
if is_plugin_public("gitea"): # False
if is_plugin_public("gitea"): # True
...
"""
import os
# 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]:

View File

@@ -3,7 +3,7 @@
Usage:
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"))
"""
@@ -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,openpanel",
"ENABLED_PLUGINS": "wordpress,woocommerce,supabase,openpanel,gitea",
"MAX_SITES_PER_USER": "10",
"USER_RATE_LIMIT_PER_MIN": "30",
"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.
# Admin users always see all plugins regardless of this setting.
# Default (if not set): wordpress,woocommerce,supabase,openpanel
# ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel
# Default (if not set): wordpress,woocommerce,supabase,openpanel,gitea
# ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel,gitea
# ============================================
# 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",
},
{
"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 ===
{
"name": "list_milestones",
@@ -499,6 +518,13 @@ async def create_label(
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
async def list_milestones(
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,
) -> str:
"""Merge a pull request"""
data = {
"Do": method,
"MergeTitleField": title,
"MergeMessageField": message,
"delete_branch_after_merge": delete_branch_after_merge,
}
data: dict = {"Do": method}
if title is not None:
data["MergeTitleField"] = title
if message is not None:
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)
result = {
"success": True,

View File

@@ -159,6 +159,79 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"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)
result = {"success": True, "webhook": webhook}
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]
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)"
authors = [
{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)
assert is_plugin_public("supabase") is True
def test_default_gitea_not_public(self):
"""Gitea is not public by default."""
def test_default_gitea_public(self):
"""Gitea is public by default (since F.16)."""
with patch.dict(os.environ, {}, clear=False):
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):
"""WordPress Advanced is not public by default."""
@@ -118,8 +118,9 @@ class TestSiteApiIntegration:
assert "wordpress" in fields
assert "woocommerce" in fields
assert "supabase" in fields
assert "gitea" in fields
assert "openpanel" in fields
assert "wordpress_advanced" not in fields
assert "gitea" not in fields
assert "n8n" not in fields
def test_get_user_plugin_names_filtered(self):
@@ -133,8 +134,9 @@ class TestSiteApiIntegration:
assert "wordpress" in names
assert "woocommerce" in names
assert "supabase" in names
assert "gitea" in names
assert "openpanel" in names
assert "wordpress_advanced" not in names
assert "gitea" not in names
def test_custom_env_changes_fields(self):
"""Custom ENABLED_PLUGINS changes which credential fields are returned."""