diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..96e46b4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,43 @@ +## Summary + + + +## Changes + +- + +## Type of Change + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update +- [ ] Refactoring (no functional changes) + +## Plugin(s) Affected + +- [ ] Core / System +- [ ] WordPress +- [ ] WooCommerce +- [ ] WordPress Advanced +- [ ] Gitea +- [ ] n8n +- [ ] Supabase +- [ ] OpenPanel +- [ ] Appwrite +- [ ] Directus +- [ ] Dashboard +- [ ] None (docs, CI, etc.) + +## Testing + +- [ ] I have run the test suite (`pytest tests/`) +- [ ] I have run linting (`black --check . && ruff check .`) +- [ ] Existing tests pass +- [ ] I have added tests for new functionality (if applicable) + +## Checklist + +- [ ] My code follows the project's coding style +- [ ] I have updated documentation (if applicable) +- [ ] I have read the [CONTRIBUTING](../CONTRIBUTING.md) guide diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..41be389 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,40 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a welcoming experience for everyone, regardless of background or +identity. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior: + +- Trolling, insulting or derogatory comments, and personal attacks +- Public or private unwelcome conduct +- Publishing others' private information without explicit permission +- Other conduct which could reasonably be considered inappropriate + +## Enforcement + +Instances of unacceptable behavior may be reported to the project maintainers +at **contact@mcphub.dev**. All complaints will be reviewed and investigated +promptly and fairly. + +Project maintainers who do not follow or enforce this Code of Conduct may face +temporary or permanent repercussions as determined by other members of the +project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct/](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). diff --git a/Dockerfile b/Dockerfile index 5f762f4..b123ca3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ WORKDIR /build COPY requirements.txt . RUN pip install --no-cache-dir --user -r requirements.txt + # Stage 2: Production stage FROM python:3.12-alpine AS production diff --git a/core/api_keys.py b/core/api_keys.py index 47b027c..3e3b792 100644 --- a/core/api_keys.py +++ b/core/api_keys.py @@ -16,12 +16,14 @@ from pathlib import Path logger = logging.getLogger(__name__) + # Valid scope values VALID_SCOPES = ["read", "write", "admin"] # Scope can be single ("read") or multiple space-separated ("read write admin") Scope = str + def validate_scope(scope: str) -> bool: """ Validate that scope contains only valid scope values. @@ -38,6 +40,7 @@ def validate_scope(scope: str) -> bool: scope_list = scope.split() return all(s in VALID_SCOPES for s in scope_list) + def normalize_scope(scope: str) -> str: """ Normalize scope string by removing duplicates and sorting. @@ -56,6 +59,7 @@ def normalize_scope(scope: str) -> str: unique_scopes.append(s) return " ".join(unique_scopes) + @dataclass class APIKey: """ @@ -105,6 +109,7 @@ class APIKey: """Check if key is valid (not revoked, not expired).""" return not self.revoked and not self.is_expired() + class APIKeyManager: """ Manages per-project API keys with persistence. @@ -487,9 +492,11 @@ class APIKeyManager: "valid": key.is_valid(), } + # Global instance _api_key_manager: APIKeyManager | None = None + def get_api_key_manager() -> APIKeyManager: """Get the global API Key Manager instance.""" global _api_key_manager diff --git a/core/audit_log.py b/core/audit_log.py index 783cfa2..284af19 100644 --- a/core/audit_log.py +++ b/core/audit_log.py @@ -19,6 +19,7 @@ from enum import Enum from pathlib import Path from typing import Any + class LogLevel(Enum): """Log severity levels.""" @@ -27,6 +28,7 @@ class LogLevel(Enum): ERROR = "ERROR" CRITICAL = "CRITICAL" + class EventType(Enum): """Types of events to log.""" @@ -36,6 +38,7 @@ class EventType(Enum): ERROR = "error" SYSTEM = "system" + class AuditLogger: """ Audit logging system for MCP operations. @@ -554,9 +557,11 @@ class AuditLogger: "log_file_size_mb": log_file_size_mb, } + # Global audit logger instance _audit_logger: AuditLogger | None = None + def get_audit_logger() -> AuditLogger: """Get the global audit logger instance.""" global _audit_logger diff --git a/core/auth.py b/core/auth.py index b0813fd..d3599e9 100644 --- a/core/auth.py +++ b/core/auth.py @@ -10,6 +10,7 @@ import secrets logger = logging.getLogger(__name__) + class AuthManager: """ Manage authentication for MCP server. @@ -115,9 +116,11 @@ class AuthManager: """Check if a project has its own API key.""" return project_id in self.project_keys + # Global authentication manager instance _auth_manager: AuthManager | None = None + def get_auth_manager() -> AuthManager: """Get the global authentication manager instance.""" global _auth_manager diff --git a/core/context.py b/core/context.py index dfd13e3..5a99aa5 100644 --- a/core/context.py +++ b/core/context.py @@ -12,6 +12,7 @@ from typing import Any # This allows unified handlers to check project access permissions _api_key_context: ContextVar[dict[str, Any] | None] = ContextVar("api_key_context", default=None) + def set_api_key_context(key_id: str, project_id: str, scope: str, is_global: bool) -> None: """ Store API key information in request context. @@ -26,6 +27,7 @@ def set_api_key_context(key_id: str, project_id: str, scope: str, is_global: boo {"key_id": key_id, "project_id": project_id, "scope": scope, "is_global": is_global} ) + def get_api_key_context() -> dict[str, Any] | None: """ Retrieve API key information from request context. @@ -35,6 +37,7 @@ def get_api_key_context() -> dict[str, Any] | None: """ return _api_key_context.get() + def clear_api_key_context() -> None: """Clear API key context (for cleanup).""" _api_key_context.set(None) diff --git a/core/dashboard/auth.py b/core/dashboard/auth.py index a01d420..4cd8b5a 100644 --- a/core/dashboard/auth.py +++ b/core/dashboard/auth.py @@ -20,6 +20,7 @@ logger = logging.getLogger(__name__) # Singleton instance _dashboard_auth: Optional["DashboardAuth"] = None + @dataclass class DashboardSession: """Dashboard session information.""" @@ -30,6 +31,7 @@ class DashboardSession: user_type: str # "master" or "api_key" key_id: str | None = None # For API key sessions + class DashboardAuth: """ Dashboard authentication manager. @@ -270,6 +272,7 @@ class DashboardAuth: ) return None + def get_dashboard_auth() -> DashboardAuth: """Get or create the singleton DashboardAuth instance.""" global _dashboard_auth diff --git a/core/dashboard/routes.py b/core/dashboard/routes.py index 2e14be5..780bc34 100644 --- a/core/dashboard/routes.py +++ b/core/dashboard/routes.py @@ -35,6 +35,7 @@ PLUGIN_DISPLAY_NAMES = { "appwrite": "Appwrite", } + def get_plugin_display_name(plugin_type: str) -> str: """Get the proper display name for a plugin type.""" if plugin_type in PLUGIN_DISPLAY_NAMES: @@ -42,6 +43,7 @@ def get_plugin_display_name(plugin_type: str) -> str: # Default: replace underscores with spaces and title case return plugin_type.replace("_", " ").title() + # Register custom Jinja filter templates.env.filters["plugin_name"] = get_plugin_display_name @@ -155,6 +157,7 @@ DASHBOARD_TRANSLATIONS = { }, } + def detect_language(accept_language: str | None, query_lang: str | None = None) -> str: """Detect language from Accept-Language header or query parameter.""" if query_lang and query_lang in DASHBOARD_TRANSLATIONS: @@ -167,10 +170,12 @@ def detect_language(accept_language: str | None, query_lang: str | None = None) return "en" + def get_translations(lang: str) -> dict: """Get translations for a language.""" return DASHBOARD_TRANSLATIONS.get(lang, DASHBOARD_TRANSLATIONS["en"]) + def get_client_ip(request: Request) -> str: """Get client IP from request, handling proxies.""" forwarded = request.headers.get("x-forwarded-for") @@ -178,6 +183,7 @@ def get_client_ip(request: Request) -> str: return forwarded.split(",")[0].strip() return request.client.host if request.client else "unknown" + async def get_dashboard_stats() -> dict: """Get dashboard statistics.""" stats = { @@ -221,6 +227,7 @@ async def get_dashboard_stats() -> dict: return stats + async def get_projects_by_type() -> dict: """Get projects grouped by plugin type.""" projects_by_type = {} @@ -242,6 +249,7 @@ async def get_projects_by_type() -> dict: return projects_by_type + async def get_recent_activity(limit: int = 10) -> list: """Get recent activity from audit logs.""" activity = [] @@ -270,6 +278,7 @@ async def get_recent_activity(limit: int = 10) -> list: return activity + async def get_health_summary() -> dict: """Get health status summary.""" summary = { @@ -302,8 +311,10 @@ async def get_health_summary() -> dict: return summary + # Route handlers + async def dashboard_login_page(request: Request) -> Response: """Render dashboard login page.""" auth = get_dashboard_auth() @@ -333,6 +344,7 @@ async def dashboard_login_page(request: Request) -> Response: }, ) + async def dashboard_login_submit(request: Request) -> Response: """Handle dashboard login form submission.""" auth = get_dashboard_auth() @@ -406,6 +418,7 @@ async def dashboard_login_submit(request: Request) -> Response: logger.info(f"Dashboard login successful: type={user_type}, ip={client_ip}") return response + async def dashboard_logout(request: Request) -> Response: """Handle dashboard logout.""" auth = get_dashboard_auth() @@ -429,6 +442,7 @@ async def dashboard_logout(request: Request) -> Response: logger.info(f"Dashboard logout from {client_ip}") return response + async def dashboard_home(request: Request) -> Response: """Render dashboard home page.""" auth = get_dashboard_auth() @@ -467,6 +481,7 @@ async def dashboard_home(request: Request) -> Response: }, ) + async def dashboard_api_stats(request: Request) -> Response: """API endpoint for dashboard stats.""" auth = get_dashboard_auth() @@ -488,10 +503,12 @@ async def dashboard_api_stats(request: Request) -> Response: } ) + # ============================================================ # Projects Routes (Phase K.2) # ============================================================ + def get_cached_health_status(project_id: str) -> dict: """ Get cached health status for a project without performing new health check. @@ -537,6 +554,7 @@ def get_cached_health_status(project_id: str) -> dict: logger.warning(f"Error getting cached health for {project_id}: {e}") return {"status": "unknown", "last_check": None, "error_rate": 0} + async def get_all_projects( plugin_type: str | None = None, search: str | None = None, @@ -615,6 +633,7 @@ async def get_all_projects( "available_plugin_types": sorted(available_plugin_types), } + async def get_project_detail(project_id: str) -> dict | None: """Get detailed information about a specific project.""" try: @@ -728,6 +747,7 @@ async def get_project_detail(project_id: str) -> dict | None: logger.warning(f"Error getting project detail: {e}") return None + async def dashboard_projects_list(request: Request) -> Response: """Render projects list page.""" auth = get_dashboard_auth() @@ -779,6 +799,7 @@ async def dashboard_projects_list(request: Request) -> Response: }, ) + async def dashboard_project_detail(request: Request) -> Response: """Render project detail page.""" auth = get_dashboard_auth() @@ -829,6 +850,7 @@ async def dashboard_project_detail(request: Request) -> Response: }, ) + async def dashboard_api_projects(request: Request) -> Response: """API endpoint for projects list.""" auth = get_dashboard_auth() @@ -851,6 +873,7 @@ async def dashboard_api_projects(request: Request) -> Response: return JSONResponse(projects_data) + async def dashboard_api_project_detail(request: Request) -> Response: """API endpoint for project detail.""" auth = get_dashboard_auth() @@ -868,6 +891,7 @@ async def dashboard_api_project_detail(request: Request) -> Response: return JSONResponse(project) + async def dashboard_project_health_check(request: Request) -> Response: """API endpoint to check health of a specific project.""" logger.debug(f"Health check request received: {request.url.path}") @@ -942,10 +966,12 @@ async def dashboard_project_health_check(request: Request) -> Response: {"status": "error", "project_id": project_id, "message": str(e)}, status_code=500 ) + # ============================================================================= # K.3: API Keys Management # ============================================================================= + async def get_all_api_keys( project_id: str | None = None, status: str = "active", @@ -1000,6 +1026,7 @@ async def get_all_api_keys( "per_page": per_page, } + async def dashboard_api_keys_list(request: Request) -> Response: """Render API keys list page.""" auth = get_dashboard_auth() @@ -1057,6 +1084,7 @@ async def dashboard_api_keys_list(request: Request) -> Response: }, ) + async def dashboard_api_keys_create(request: Request) -> Response: """API endpoint to create a new API key.""" auth = get_dashboard_auth() @@ -1111,6 +1139,7 @@ async def dashboard_api_keys_create(request: Request) -> Response: logger.error(f"Error creating API key: {e}") return JSONResponse({"error": "Failed to create key"}, status_code=500) + async def dashboard_api_keys_revoke(request: Request) -> Response: """API endpoint to revoke an API key.""" auth = get_dashboard_auth() @@ -1144,6 +1173,7 @@ async def dashboard_api_keys_revoke(request: Request) -> Response: logger.error(f"Error revoking API key: {e}") return JSONResponse({"error": "Failed to revoke key"}, status_code=500) + async def dashboard_api_keys_delete(request: Request) -> Response: """API endpoint to delete an API key.""" auth = get_dashboard_auth() @@ -1177,10 +1207,12 @@ async def dashboard_api_keys_delete(request: Request) -> Response: logger.error(f"Error deleting API key: {e}") return JSONResponse({"error": "Failed to delete key"}, status_code=500) + # ============================================================================= # K.4: OAuth Clients Management # ============================================================================= + async def get_oauth_clients_data() -> dict: """Get OAuth clients data.""" from core.oauth.client_registry import get_client_registry @@ -1213,6 +1245,7 @@ async def get_oauth_clients_data() -> dict: "total_count": 0, } + async def dashboard_oauth_clients_list(request: Request) -> Response: """Render OAuth clients list page.""" auth = get_dashboard_auth() @@ -1246,6 +1279,7 @@ async def dashboard_oauth_clients_list(request: Request) -> Response: }, ) + async def dashboard_oauth_clients_create(request: Request) -> Response: """API endpoint to create OAuth client.""" auth = get_dashboard_auth() @@ -1295,6 +1329,7 @@ async def dashboard_oauth_clients_create(request: Request) -> Response: logger.error(f"Error creating OAuth client: {e}") return JSONResponse({"error": "Failed to create client"}, status_code=500) + async def dashboard_oauth_clients_delete(request: Request) -> Response: """API endpoint to delete OAuth client.""" auth = get_dashboard_auth() @@ -1329,10 +1364,12 @@ async def dashboard_oauth_clients_delete(request: Request) -> Response: logger.error(f"Error deleting OAuth client: {e}") return JSONResponse({"error": "Failed to delete client"}, status_code=500) + # ============================================================================= # K.4: Audit Logs Management # ============================================================================= + async def get_audit_logs_data( event_type: str | None = None, level: str | None = None, @@ -1450,6 +1487,7 @@ async def get_audit_logs_data( "per_page": per_page, } + async def dashboard_audit_logs_list(request: Request) -> Response: """Render audit logs list page.""" auth = get_dashboard_auth() @@ -1514,6 +1552,7 @@ async def dashboard_audit_logs_list(request: Request) -> Response: }, ) + async def dashboard_api_audit_logs(request: Request) -> Response: """API endpoint for audit logs list.""" auth = get_dashboard_auth() @@ -1545,10 +1584,12 @@ async def dashboard_api_audit_logs(request: Request) -> Response: logger.error(f"Error getting audit logs: {e}") return JSONResponse({"error": "Failed to get logs"}, status_code=500) + # ============================================================================= # K.5: Health Monitoring # ============================================================================= + def get_basic_health_data() -> dict: """Get basic health data (fast, no project checks).""" try: @@ -1618,6 +1659,7 @@ def get_basic_health_data() -> dict: }, } + def get_cached_projects_health() -> dict: """ Get cached health status for all projects without live checks. @@ -1679,6 +1721,7 @@ def get_cached_projects_health() -> dict: "alerts": [], # No alerts from cached data } + async def get_health_data(live_check: bool = True) -> dict: """ Get comprehensive health monitoring data. @@ -1777,6 +1820,7 @@ async def get_health_data(live_check: bool = True) -> dict: logger.error(f"Error getting health data: {e}") return default_result + async def dashboard_health_page(request: Request) -> Response: """ Render health monitoring page. @@ -1831,6 +1875,7 @@ async def dashboard_health_page(request: Request) -> Response: logger.error(traceback.format_exc()) return HTMLResponse(f"
{e}", status_code=500)
+
async def dashboard_health_projects_partial(request: Request) -> Response:
"""HTMX endpoint for projects health data (HTML partial)."""
logger.debug("Health projects partial endpoint called")
@@ -1882,6 +1927,7 @@ async def dashboard_health_projects_partial(request: Request) -> Response:
status_code=500,
)
+
async def dashboard_api_health(request: Request) -> Response:
"""API endpoint for health data."""
auth = get_dashboard_auth()
@@ -1898,10 +1944,12 @@ async def dashboard_api_health(request: Request) -> Response:
logger.error(f"Error getting health data: {e}")
return JSONResponse({"error": "Failed to get health data"}, status_code=500)
+
# =============================================================================
# K.5: Settings Page
# =============================================================================
+
def get_system_config() -> dict:
"""Get system configuration for display."""
@@ -1919,6 +1967,7 @@ def get_system_config() -> dict:
"dashboard_session_expiry": os.environ.get("DASHBOARD_SESSION_EXPIRY_HOURS", "24"),
}
+
def get_registered_plugins() -> list:
"""Get list of registered plugins."""
plugins = []
@@ -1941,6 +1990,7 @@ def get_registered_plugins() -> list:
return plugins
+
def get_about_info() -> dict:
"""Get about information."""
import sys
@@ -1961,6 +2011,7 @@ def get_about_info() -> dict:
"tools_count": tools_count,
}
+
async def dashboard_settings_page(request: Request) -> Response:
"""Render settings page."""
auth = get_dashboard_auth()
@@ -2004,6 +2055,7 @@ async def dashboard_settings_page(request: Request) -> Response:
},
)
+
def register_dashboard_routes(mcp):
"""
Register dashboard routes with the MCP server.
diff --git a/core/endpoints/config.py b/core/endpoints/config.py
index 5401239..ce39d4d 100644
--- a/core/endpoints/config.py
+++ b/core/endpoints/config.py
@@ -8,6 +8,7 @@ Each endpoint has specific plugin types, scopes, and access requirements.
from dataclasses import dataclass, field
from enum import Enum
+
class EndpointType(Enum):
"""Types of MCP endpoints"""
@@ -25,6 +26,7 @@ class EndpointType(Enum):
PROJECT = "project" # Dynamic per-project endpoint
CUSTOM = "custom"
+
@dataclass
class EndpointConfig:
"""
@@ -90,6 +92,7 @@ class EndpointConfig:
return True # Empty set = all scopes
return scope in self.allowed_scopes
+
# Predefined endpoint configurations
ENDPOINT_CONFIGS = {
# Admin endpoint - all tools, requires Master API Key
@@ -305,12 +308,14 @@ ENDPOINT_CONFIGS = {
),
}
+
def get_endpoint_config(endpoint_type: EndpointType) -> EndpointConfig:
"""Get configuration for a specific endpoint type"""
if endpoint_type not in ENDPOINT_CONFIGS:
raise ValueError(f"Unknown endpoint type: {endpoint_type}")
return ENDPOINT_CONFIGS[endpoint_type]
+
def create_project_endpoint_config(
project_id: str, plugin_type: str, site_alias: str | None = None
) -> EndpointConfig:
diff --git a/core/endpoints/factory.py b/core/endpoints/factory.py
index b247825..d9eb6e5 100644
--- a/core/endpoints/factory.py
+++ b/core/endpoints/factory.py
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
+
class MCPEndpointFactory:
"""
Factory for creating scoped MCP endpoints.
diff --git a/core/endpoints/middleware.py b/core/endpoints/middleware.py
index 816382e..0e05672 100644
--- a/core/endpoints/middleware.py
+++ b/core/endpoints/middleware.py
@@ -24,6 +24,7 @@ from .config import EndpointConfig
logger = logging.getLogger(__name__)
+
@dataclass
class AuthContext:
"""Authentication context for a request"""
@@ -35,6 +36,7 @@ class AuthContext:
is_oauth_token: bool = False
client_ip: str | None = None
+
class EndpointAuthMiddleware(Middleware):
"""
Authentication middleware for multi-endpoint architecture.
@@ -256,6 +258,7 @@ class EndpointAuthMiddleware(Middleware):
duration_ms = int((time.time() - start_time) * 1000)
logger.warning(f"Tool {tool_name} failed: {error} (duration={duration_ms}ms)")
+
class EndpointRateLimitMiddleware(Middleware):
"""
Rate limiting middleware for multi-endpoint architecture.
@@ -285,6 +288,7 @@ class EndpointRateLimitMiddleware(Middleware):
# Proceed with request
return await call_next(context)
+
class EndpointAuditMiddleware(Middleware):
"""
Audit logging middleware for multi-endpoint architecture.
@@ -333,6 +337,7 @@ class EndpointAuditMiddleware(Middleware):
)
raise
+
def create_endpoint_middleware(endpoint_config: EndpointConfig) -> list:
"""
Create middleware stack for an endpoint.
diff --git a/core/endpoints/registry.py b/core/endpoints/registry.py
index 496a70a..34773fb 100644
--- a/core/endpoints/registry.py
+++ b/core/endpoints/registry.py
@@ -21,6 +21,7 @@ from .factory import MCPEndpointFactory
logger = logging.getLogger(__name__)
+
@dataclass
class EndpointInfo:
"""Information about a registered endpoint"""
@@ -33,6 +34,7 @@ class EndpointInfo:
plugin_types: list[str]
require_master_key: bool
+
class EndpointRegistry:
"""
Central registry for all MCP endpoints.
@@ -199,9 +201,11 @@ class EndpointRegistry:
logger.info(f"Total: {len(self._endpoints)} endpoints")
logger.info("=" * 60)
+
# Singleton instance
_registry: EndpointRegistry | None = None
+
def get_endpoint_registry() -> EndpointRegistry:
"""Get the global endpoint registry instance"""
global _registry
@@ -211,6 +215,7 @@ def get_endpoint_registry() -> EndpointRegistry:
)
return _registry
+
def initialize_endpoint_registry(factory: MCPEndpointFactory) -> EndpointRegistry:
"""
Initialize the global endpoint registry.
diff --git a/core/health.py b/core/health.py
index 5f917af..3c1d4fa 100644
--- a/core/health.py
+++ b/core/health.py
@@ -27,6 +27,7 @@ from core.project_manager import ProjectManager
logger = logging.getLogger(__name__)
+
@dataclass
class HealthMetric:
"""Individual health metric data point."""
@@ -47,6 +48,7 @@ class HealthMetric:
"error_message": self.error_message,
}
+
@dataclass
class SystemMetrics:
"""System-wide metrics."""
@@ -63,6 +65,7 @@ class SystemMetrics:
"""Convert to dictionary."""
return asdict(self)
+
@dataclass
class ProjectHealthStatus:
"""Comprehensive health status for a project."""
@@ -89,6 +92,7 @@ class ProjectHealthStatus:
"details": self.details,
}
+
@dataclass
class AlertThreshold:
"""Alert threshold configuration."""
@@ -109,6 +113,7 @@ class AlertThreshold:
return value == self.threshold
return False
+
class HealthMonitor:
"""
Enhanced health monitoring system with metrics tracking and alerting.
@@ -657,13 +662,16 @@ class HealthMonitor:
self.request_timestamps.clear()
logger.warning("All metrics have been reset")
+
# Singleton instance
_health_monitor: HealthMonitor | None = None
+
def get_health_monitor() -> HealthMonitor | None:
"""Get the global health monitor instance."""
return _health_monitor
+
def initialize_health_monitor(
project_manager: ProjectManager, audit_logger: AuditLogger | None = None, **kwargs
) -> HealthMonitor:
diff --git a/core/i18n.py b/core/i18n.py
index 1d7decf..25278b8 100644
--- a/core/i18n.py
+++ b/core/i18n.py
@@ -89,6 +89,7 @@ TRANSLATIONS: dict[str, dict[str, str]] = {
},
}
+
def detect_language(accept_language: str | None = None, query_lang: str | None = None) -> str:
"""
Detect user's preferred language from Accept-Language header or query parameter.
@@ -131,6 +132,7 @@ def detect_language(accept_language: str | None = None, query_lang: str | None =
# Default: English
return "en"
+
def get_translation(lang: str, key: str, **kwargs) -> str:
"""
Get translated string for the given language and key.
@@ -166,6 +168,7 @@ def get_translation(lang: str, key: str, **kwargs) -> str:
return text
+
def get_all_translations(lang: str) -> dict[str, str]:
"""
Get all translations for a language as a dictionary.
@@ -180,6 +183,7 @@ def get_all_translations(lang: str) -> dict[str, str]:
"""
return TRANSLATIONS.get(lang, TRANSLATIONS["en"])
+
def get_language_name(lang: str) -> str:
"""
Get human-readable language name.
diff --git a/core/oauth/client_registry.py b/core/oauth/client_registry.py
index f3379ed..15fc47f 100644
--- a/core/oauth/client_registry.py
+++ b/core/oauth/client_registry.py
@@ -13,6 +13,7 @@ from .schemas import OAuthClient
logger = logging.getLogger(__name__)
+
class ClientRegistry:
"""
OAuth Client Registry with JSON storage.
@@ -143,9 +144,11 @@ class ClientRegistry:
return True
return False
+
# Singleton instance
_client_registry: ClientRegistry | None = None
+
def get_client_registry() -> ClientRegistry:
"""Get singleton ClientRegistry instance"""
global _client_registry
diff --git a/core/oauth/csrf.py b/core/oauth/csrf.py
index a2dce61..b8a0305 100644
--- a/core/oauth/csrf.py
+++ b/core/oauth/csrf.py
@@ -10,6 +10,7 @@ Part of Phase E: Custom OAuth Authorization Page
import secrets
import time
+
class CSRFTokenManager:
"""
Manages CSRF tokens for OAuth authorization requests.
@@ -107,9 +108,11 @@ class CSRFTokenManager:
"token_lifetime_seconds": self._token_lifetime,
}
+
# Global CSRF token manager instance
_csrf_manager: CSRFTokenManager | None = None
+
def get_csrf_manager() -> CSRFTokenManager:
"""
Get the global CSRF token manager instance.
diff --git a/core/oauth/pkce.py b/core/oauth/pkce.py
index 35b2f95..efb9bbb 100644
--- a/core/oauth/pkce.py
+++ b/core/oauth/pkce.py
@@ -8,6 +8,7 @@ import hashlib
import secrets
from typing import Literal
+
def generate_code_verifier(length: int = 64) -> str:
"""
Generate PKCE code verifier.
@@ -34,6 +35,7 @@ def generate_code_verifier(length: int = 64) -> str:
return verifier
+
def generate_code_challenge(code_verifier: str, method: Literal["S256"] = "S256") -> str:
"""
Generate PKCE code challenge from verifier.
@@ -63,6 +65,7 @@ def generate_code_challenge(code_verifier: str, method: Literal["S256"] = "S256"
return challenge
+
def validate_code_challenge(
code_verifier: str, code_challenge: str, method: Literal["S256"] = "S256"
) -> bool:
diff --git a/core/oauth/schemas.py b/core/oauth/schemas.py
index fc351e6..5c96236 100644
--- a/core/oauth/schemas.py
+++ b/core/oauth/schemas.py
@@ -6,6 +6,7 @@ from datetime import UTC, datetime
from pydantic import BaseModel, Field, field_validator
+
class OAuthClient(BaseModel):
"""OAuth 2.1 Client Model"""
@@ -44,6 +45,7 @@ class OAuthClient(BaseModel):
raise ValueError(f"Invalid grant type: {grant}")
return v
+
class AuthorizationCode(BaseModel):
"""Authorization Code Model"""
@@ -77,6 +79,7 @@ class AuthorizationCode(BaseModel):
raise ValueError("Only S256 code_challenge_method is supported (OAuth 2.1)")
return v
+
class AccessToken(BaseModel):
"""Access Token Model"""
@@ -96,6 +99,7 @@ class AccessToken(BaseModel):
expires = expires.replace(tzinfo=UTC)
return now > expires
+
class RefreshToken(BaseModel):
"""Refresh Token Model"""
@@ -115,6 +119,7 @@ class RefreshToken(BaseModel):
expires = expires.replace(tzinfo=UTC)
return now > expires
+
class TokenRequest(BaseModel):
"""Token Request Model"""
@@ -135,6 +140,7 @@ class TokenRequest(BaseModel):
client_secret: str | None = None
scope: str | None = None
+
class TokenResponse(BaseModel):
"""Token Response Model"""
diff --git a/core/oauth/server.py b/core/oauth/server.py
index bbe82b0..d6e93b8 100644
--- a/core/oauth/server.py
+++ b/core/oauth/server.py
@@ -16,6 +16,7 @@ from .token_manager import get_token_manager
logger = logging.getLogger(__name__)
+
class OAuthError(Exception):
"""OAuth error with error code and description"""
@@ -25,6 +26,7 @@ class OAuthError(Exception):
self.status_code = status_code
super().__init__(error_description)
+
class OAuthServer:
"""
OAuth 2.1 Authorization Server
@@ -385,9 +387,11 @@ class OAuthServer:
scope=" ".join(requested_scopes),
)
+
# Singleton
_oauth_server: OAuthServer | None = None
+
def get_oauth_server() -> OAuthServer:
"""Get singleton OAuthServer instance"""
global _oauth_server
diff --git a/core/oauth/storage.py b/core/oauth/storage.py
index c9d716f..14379d7 100644
--- a/core/oauth/storage.py
+++ b/core/oauth/storage.py
@@ -13,6 +13,7 @@ from .schemas import AccessToken, AuthorizationCode, RefreshToken
logger = logging.getLogger(__name__)
+
class BaseStorage:
"""Base storage interface"""
@@ -43,6 +44,7 @@ class BaseStorage:
def revoke_refresh_token(self, token: str) -> bool:
raise NotImplementedError
+
class JSONStorage(BaseStorage):
"""
JSON file storage for OAuth tokens.
@@ -199,6 +201,7 @@ class JSONStorage(BaseStorage):
logger.info(f"Cleaned up {len(codes) - len(cleaned_codes)} expired authorization codes")
logger.info(f"Cleaned up {len(tokens) - len(cleaned_tokens)} expired access tokens")
+
def get_storage() -> BaseStorage:
"""
Get storage instance based on environment.
diff --git a/core/oauth/token_manager.py b/core/oauth/token_manager.py
index 1db9556..39ecc70 100644
--- a/core/oauth/token_manager.py
+++ b/core/oauth/token_manager.py
@@ -17,11 +17,13 @@ from .storage import get_storage
logger = logging.getLogger(__name__)
+
class SecurityError(Exception):
"""Security-related error (e.g., token reuse)"""
pass
+
class TokenManager:
"""
OAuth 2.1 Token Manager.
@@ -302,9 +304,11 @@ class TokenManager:
# Access tokens cannot be revoked (JWT stateless)
# They expire naturally after TTL
+
# Singleton
_token_manager: TokenManager | None = None
+
def get_token_manager() -> TokenManager:
"""Get singleton TokenManager instance"""
global _token_manager
diff --git a/core/project_manager.py b/core/project_manager.py
index f002dc8..c2ef186 100644
--- a/core/project_manager.py
+++ b/core/project_manager.py
@@ -14,6 +14,7 @@ from plugins import BasePlugin, registry
logger = logging.getLogger(__name__)
+
class ProjectManager:
"""
Manage multiple project instances.
@@ -233,9 +234,11 @@ class ProjectManager:
for full_id, plugin in self.projects.items()
]
+
# Global project manager instance
_project_manager: ProjectManager | None = None
+
def get_project_manager() -> ProjectManager:
"""Get the global project manager instance."""
global _project_manager
diff --git a/core/rate_limiter.py b/core/rate_limiter.py
index e8edee5..c001bcd 100644
--- a/core/rate_limiter.py
+++ b/core/rate_limiter.py
@@ -24,6 +24,7 @@ from typing import Any
logger = logging.getLogger(__name__)
+
@dataclass
class RateLimitConfig:
"""Configuration for rate limits at different time intervals."""
@@ -42,6 +43,7 @@ class RateLimitConfig:
per_day=int(os.getenv(f"{env_prefix}RATE_LIMIT_PER_DAY", "10000")),
)
+
@dataclass
class TokenBucket:
"""
@@ -110,6 +112,7 @@ class TokenBucket:
tokens_needed = tokens - self.tokens
return tokens_needed / self.refill_rate
+
@dataclass
class ClientRateLimitState:
"""Track rate limit state for a single client."""
@@ -188,6 +191,7 @@ class ClientRateLimitState:
"uptime_seconds": uptime,
}
+
class RateLimiter:
"""
Rate limiter using Token Bucket algorithm.
@@ -403,9 +407,11 @@ class RateLimiter:
f"{config.per_minute}/min, {config.per_hour}/hour, {config.per_day}/day"
)
+
# Singleton instance
_rate_limiter: RateLimiter | None = None
+
def get_rate_limiter() -> RateLimiter:
"""Get or create the global rate limiter instance."""
global _rate_limiter
diff --git a/core/site_manager.py b/core/site_manager.py
index b211641..bbe94f1 100644
--- a/core/site_manager.py
+++ b/core/site_manager.py
@@ -24,6 +24,7 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validat
logger = logging.getLogger(__name__)
+
class SiteConfig(BaseModel):
"""
Type-safe site configuration.
@@ -104,6 +105,7 @@ class SiteConfig(BaseModel):
"""
return self.model_dump()
+
class SiteManager:
"""
Manage site configurations with type safety.
@@ -519,9 +521,11 @@ class SiteManager:
counts_str = ", ".join(f"{k}: {v}" for k, v in counts.items())
return f"SiteManager(total={self.get_count()}, {counts_str})"
+
# Singleton instance
_site_manager: SiteManager | None = None
+
def get_site_manager() -> SiteManager:
"""
Get the singleton site manager instance.
diff --git a/core/site_registry.py b/core/site_registry.py
index 27f83ed..e2f537c 100644
--- a/core/site_registry.py
+++ b/core/site_registry.py
@@ -12,6 +12,7 @@ from typing import Any
logger = logging.getLogger(__name__)
+
class SiteInfo:
"""Information about a single site."""
@@ -50,6 +51,7 @@ class SiteInfo:
"config_keys": list(self.config.keys()),
}
+
class SiteRegistry:
"""
Registry for managing site configurations across plugin types.
@@ -360,9 +362,11 @@ class SiteRegistry:
options.add(site_info.alias)
return sorted(options)
+
# Global site registry instance
_site_registry: SiteRegistry | None = None
+
def get_site_registry() -> SiteRegistry:
"""Get the global site registry instance."""
global _site_registry
diff --git a/core/tool_generator.py b/core/tool_generator.py
index 7573209..7101db3 100644
--- a/core/tool_generator.py
+++ b/core/tool_generator.py
@@ -14,6 +14,7 @@ from core.tool_registry import ToolDefinition
logger = logging.getLogger(__name__)
+
# Plugin type fallback mapping - used when a plugin has no sites configured
# WooCommerce can fallback to WordPress sites (same URL, credentials)
# NOTE: Using fallback is NOT recommended in production. Always configure
@@ -23,6 +24,7 @@ PLUGIN_SITE_FALLBACK = {
# Add more fallbacks as needed
}
+
def get_site_plugin_type_with_fallback(plugin_type: str, site_manager) -> str:
"""
Get the site configuration plugin type for a given plugin.
@@ -60,6 +62,7 @@ def get_site_plugin_type_with_fallback(plugin_type: str, site_manager) -> str:
# Return original type (may have no sites)
return plugin_type
+
class ToolGenerator:
"""
Generate tools directly from plugin classes.
diff --git a/core/tool_registry.py b/core/tool_registry.py
index 3c9bdeb..7e5db1b 100644
--- a/core/tool_registry.py
+++ b/core/tool_registry.py
@@ -13,6 +13,7 @@ from pydantic import BaseModel, ConfigDict, Field
logger = logging.getLogger(__name__)
+
class ToolDefinition(BaseModel):
"""
Type-safe tool definition.
@@ -42,6 +43,7 @@ class ToolDefinition(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True) # Allow Callable type
+
class ToolRegistry:
"""
Central registry for all MCP tools.
@@ -208,9 +210,11 @@ class ToolRegistry:
counts_str = ", ".join(f"{k}: {v}" for k, v in counts.items())
return f"ToolRegistry(total={self.get_count()}, {counts_str})"
+
# Singleton instance
_tool_registry: ToolRegistry | None = None
+
def get_tool_registry() -> ToolRegistry:
"""
Get the singleton tool registry instance.
diff --git a/core/unified_tools.py b/core/unified_tools.py
index d5e9135..2774a17 100644
--- a/core/unified_tools.py
+++ b/core/unified_tools.py
@@ -18,6 +18,7 @@ from core.site_registry import get_site_registry
logger = logging.getLogger(__name__)
+
class UnifiedToolGenerator:
"""
Generates unified tools from per-site tool definitions.
diff --git a/examples/basic_usage.py b/examples/basic_usage.py
index b1824c0..5d29821 100644
--- a/examples/basic_usage.py
+++ b/examples/basic_usage.py
@@ -13,6 +13,7 @@ from dotenv import load_dotenv
# Load environment variables
load_dotenv()
+
# Example 1: List Posts
async def list_posts_example():
"""List published posts from a WordPress site."""
@@ -39,6 +40,7 @@ async def list_posts_example():
except Exception as e:
print(f"Error: {e}")
+
# Example 2: Create a Post
async def create_post_example():
"""Create a new WordPress post."""
@@ -78,6 +80,7 @@ async def create_post_example():
print(f"Error: {e}")
return None
+
# Example 3: Update a Post
async def update_post_example(post_id):
"""Update an existing WordPress post."""
@@ -118,6 +121,7 @@ async def update_post_example(post_id):
except Exception as e:
print(f"Error: {e}")
+
# Example 4: List Categories
async def list_categories_example():
"""List WordPress categories."""
@@ -141,6 +145,7 @@ async def list_categories_example():
except Exception as e:
print(f"Error: {e}")
+
# Example 5: Upload Media
async def upload_media_example():
"""Upload media from URL to WordPress."""
@@ -178,6 +183,7 @@ async def upload_media_example():
print(f"Error: {e}")
return None
+
# Example 6: Get Site Health
async def site_health_example():
"""Check WordPress site health."""
@@ -200,6 +206,7 @@ async def site_health_example():
except Exception as e:
print(f"Error: {e}")
+
# Example 7: Get System Metrics
async def system_metrics_example():
"""Check MCP server health and metrics."""
@@ -224,6 +231,7 @@ async def system_metrics_example():
except Exception as e:
print(f"Error: {e}")
+
# Example 8: Delete Post
async def delete_post_example(post_id):
"""Delete a WordPress post."""
@@ -257,6 +265,7 @@ async def delete_post_example(post_id):
except Exception as e:
print(f"Error: {e}")
+
# Main execution
async def main():
"""Run all examples in sequence."""
@@ -289,5 +298,6 @@ async def main():
print("Examples completed successfully!")
print("=" * 60 + "\n")
+
if __name__ == "__main__":
asyncio.run(main())
diff --git a/plugins/appwrite/client.py b/plugins/appwrite/client.py
index 41fd818..d95652a 100644
--- a/plugins/appwrite/client.py
+++ b/plugins/appwrite/client.py
@@ -21,6 +21,7 @@ from typing import Any
import aiohttp
+
class AppwriteClient:
"""
Appwrite Self-Hosted API client.
diff --git a/plugins/appwrite/handlers/databases.py b/plugins/appwrite/handlers/databases.py
index a96c257..eb51822 100644
--- a/plugins/appwrite/handlers/databases.py
+++ b/plugins/appwrite/handlers/databases.py
@@ -13,6 +13,7 @@ from typing import Any
from plugins.appwrite.client import AppwriteClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (18 tools)"""
return [
@@ -425,10 +426,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_databases(
client: AppwriteClient, queries: list[str] | None = None, search: str | None = None
) -> str:
@@ -447,6 +450,7 @@ async def list_databases(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_database(client: AppwriteClient, database_id: str) -> str:
"""Get database by ID."""
try:
@@ -456,6 +460,7 @@ async def get_database(client: AppwriteClient, database_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_database(
client: AppwriteClient, database_id: str, name: str, enabled: bool = True
) -> str:
@@ -475,6 +480,7 @@ async def create_database(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_database(
client: AppwriteClient, database_id: str, name: str, enabled: bool | None = None
) -> str:
@@ -490,6 +496,7 @@ async def update_database(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_database(client: AppwriteClient, database_id: str) -> str:
"""Delete database."""
try:
@@ -501,6 +508,7 @@ async def delete_database(client: AppwriteClient, database_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_collections(
client: AppwriteClient,
database_id: str,
@@ -525,6 +533,7 @@ async def list_collections(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_collection(client: AppwriteClient, database_id: str, collection_id: str) -> str:
"""Get collection details."""
try:
@@ -534,6 +543,7 @@ async def get_collection(client: AppwriteClient, database_id: str, collection_id
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_collection(
client: AppwriteClient,
database_id: str,
@@ -566,6 +576,7 @@ async def create_collection(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_collection(
client: AppwriteClient,
database_id: str,
@@ -594,6 +605,7 @@ async def update_collection(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_collection(client: AppwriteClient, database_id: str, collection_id: str) -> str:
"""Delete collection."""
try:
@@ -606,6 +618,7 @@ async def delete_collection(client: AppwriteClient, database_id: str, collection
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_attributes(
client: AppwriteClient, database_id: str, collection_id: str, queries: list[str] | None = None
) -> str:
@@ -628,6 +641,7 @@ async def list_attributes(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_string_attribute(
client: AppwriteClient,
database_id: str,
@@ -664,6 +678,7 @@ async def create_string_attribute(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_integer_attribute(
client: AppwriteClient,
database_id: str,
@@ -700,6 +715,7 @@ async def create_integer_attribute(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_boolean_attribute(
client: AppwriteClient,
database_id: str,
@@ -732,6 +748,7 @@ async def create_boolean_attribute(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_attribute(
client: AppwriteClient, database_id: str, collection_id: str, key: str
) -> str:
@@ -745,6 +762,7 @@ async def delete_attribute(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_indexes(
client: AppwriteClient, database_id: str, collection_id: str, queries: list[str] | None = None
) -> str:
@@ -767,6 +785,7 @@ async def list_indexes(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_index(
client: AppwriteClient,
database_id: str,
@@ -795,6 +814,7 @@ async def create_index(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_index(
client: AppwriteClient, database_id: str, collection_id: str, key: str
) -> str:
diff --git a/plugins/appwrite/handlers/documents.py b/plugins/appwrite/handlers/documents.py
index 7a2ed4d..2436bbb 100644
--- a/plugins/appwrite/handlers/documents.py
+++ b/plugins/appwrite/handlers/documents.py
@@ -16,38 +16,47 @@ from plugins.appwrite.client import AppwriteClient
# QUERY HELPERS (Appwrite 1.7.4 JSON format)
# =====================
+
def _query_limit(value: int) -> str:
"""Build limit query in JSON format."""
return json.dumps({"method": "limit", "values": [value]})
+
def _query_offset(value: int) -> str:
"""Build offset query in JSON format."""
return json.dumps({"method": "offset", "values": [value]})
+
def _query_order_asc(attribute: str) -> str:
"""Build orderAsc query in JSON format."""
return json.dumps({"method": "orderAsc", "values": [attribute]})
+
def _query_order_desc(attribute: str) -> str:
"""Build orderDesc query in JSON format."""
return json.dumps({"method": "orderDesc", "values": [attribute]})
+
def _query_cursor_after(document_id: str) -> str:
"""Build cursorAfter query in JSON format."""
return json.dumps({"method": "cursorAfter", "values": [document_id]})
+
def _query_cursor_before(document_id: str) -> str:
"""Build cursorBefore query in JSON format."""
return json.dumps({"method": "cursorBefore", "values": [document_id]})
+
def _query_search(attribute: str, value: str) -> str:
"""Build search query in JSON format."""
return json.dumps({"method": "search", "attribute": attribute, "values": [value]})
+
def _query_equal(attribute: str, values: list[Any]) -> str:
"""Build equal query in JSON format."""
return json.dumps({"method": "equal", "attribute": attribute, "values": values})
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (12 tools)"""
return [
@@ -381,10 +390,12 @@ Each document must have:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_documents(
client: AppwriteClient, database_id: str, collection_id: str, queries: list[str] | None = None
) -> str:
@@ -407,6 +418,7 @@ async def list_documents(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_document(
client: AppwriteClient,
database_id: str,
@@ -427,6 +439,7 @@ async def get_document(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_document(
client: AppwriteClient,
database_id: str,
@@ -453,6 +466,7 @@ async def create_document(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_document(
client: AppwriteClient,
database_id: str,
@@ -479,6 +493,7 @@ async def update_document(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_document(
client: AppwriteClient, database_id: str, collection_id: str, document_id: str
) -> str:
@@ -492,6 +507,7 @@ async def delete_document(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def bulk_create_documents(
client: AppwriteClient, database_id: str, collection_id: str, documents: list[dict[str, Any]]
) -> str:
@@ -527,6 +543,7 @@ async def bulk_create_documents(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def bulk_update_documents(
client: AppwriteClient, database_id: str, collection_id: str, updates: list[dict[str, Any]]
) -> str:
@@ -562,6 +579,7 @@ async def bulk_update_documents(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def bulk_delete_documents(
client: AppwriteClient, database_id: str, collection_id: str, document_ids: list[str]
) -> str:
@@ -591,6 +609,7 @@ async def bulk_delete_documents(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def search_documents(
client: AppwriteClient,
database_id: str,
@@ -623,6 +642,7 @@ async def search_documents(
error_msg += " (Hint: Make sure you have a fulltext index on the searched attribute)"
return json.dumps({"success": False, "error": error_msg}, indent=2)
+
async def count_documents(
client: AppwriteClient, database_id: str, collection_id: str, queries: list[str] | None = None
) -> str:
@@ -653,6 +673,7 @@ async def count_documents(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_documents_by_ids(
client: AppwriteClient, database_id: str, collection_id: str, document_ids: list[str]
) -> str:
@@ -684,6 +705,7 @@ async def get_documents_by_ids(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_documents_paginated(
client: AppwriteClient,
database_id: str,
diff --git a/plugins/appwrite/handlers/functions.py b/plugins/appwrite/handlers/functions.py
index f0dc877..63550be 100644
--- a/plugins/appwrite/handlers/functions.py
+++ b/plugins/appwrite/handlers/functions.py
@@ -12,6 +12,7 @@ from typing import Any
from plugins.appwrite.client import AppwriteClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (14 tools)"""
return [
@@ -337,10 +338,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_functions(
client: AppwriteClient, queries: list[str] | None = None, search: str | None = None
) -> str:
@@ -359,6 +362,7 @@ async def list_functions(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_function(client: AppwriteClient, function_id: str) -> str:
"""Get function by ID."""
try:
@@ -368,6 +372,7 @@ async def get_function(client: AppwriteClient, function_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_function(
client: AppwriteClient,
function_id: str,
@@ -410,6 +415,7 @@ async def create_function(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_function(
client: AppwriteClient,
function_id: str,
@@ -442,6 +448,7 @@ async def update_function(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_function(client: AppwriteClient, function_id: str) -> str:
"""Delete function."""
try:
@@ -453,6 +460,7 @@ async def delete_function(client: AppwriteClient, function_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_deployments(
client: AppwriteClient,
function_id: str,
@@ -477,6 +485,7 @@ async def list_deployments(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_deployment(client: AppwriteClient, function_id: str, deployment_id: str) -> str:
"""Get deployment by ID."""
try:
@@ -486,6 +495,7 @@ async def get_deployment(client: AppwriteClient, function_id: str, deployment_id
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_deployment(client: AppwriteClient, function_id: str, deployment_id: str) -> str:
"""Delete deployment."""
try:
@@ -498,6 +508,7 @@ async def delete_deployment(client: AppwriteClient, function_id: str, deployment
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def activate_deployment(client: AppwriteClient, function_id: str, deployment_id: str) -> str:
"""Activate deployment."""
try:
@@ -515,6 +526,7 @@ async def activate_deployment(client: AppwriteClient, function_id: str, deployme
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_active_deployment(client: AppwriteClient, function_id: str) -> str:
"""Get active deployment for function."""
try:
@@ -542,6 +554,7 @@ async def get_active_deployment(client: AppwriteClient, function_id: str) -> str
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_executions(
client: AppwriteClient,
function_id: str,
@@ -566,6 +579,7 @@ async def list_executions(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_execution(client: AppwriteClient, function_id: str, execution_id: str) -> str:
"""Get execution by ID."""
try:
@@ -575,6 +589,7 @@ async def get_execution(client: AppwriteClient, function_id: str, execution_id:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def execute_function(
client: AppwriteClient,
function_id: str,
@@ -616,6 +631,7 @@ async def execute_function(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_execution(client: AppwriteClient, function_id: str, execution_id: str) -> str:
"""Delete execution."""
try:
diff --git a/plugins/appwrite/handlers/messaging.py b/plugins/appwrite/handlers/messaging.py
index dc43072..20566b1 100644
--- a/plugins/appwrite/handlers/messaging.py
+++ b/plugins/appwrite/handlers/messaging.py
@@ -12,6 +12,7 @@ from typing import Any
from plugins.appwrite.client import AppwriteClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (12 tools)"""
return [
@@ -316,10 +317,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_topics(
client: AppwriteClient, queries: list[str] | None = None, search: str | None = None
) -> str:
@@ -334,6 +337,7 @@ async def list_topics(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_topic(client: AppwriteClient, topic_id: str) -> str:
"""Get topic by ID."""
try:
@@ -343,6 +347,7 @@ async def get_topic(client: AppwriteClient, topic_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_topic(
client: AppwriteClient, topic_id: str, name: str, subscribe: list[str] | None = None
) -> str:
@@ -358,6 +363,7 @@ async def create_topic(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_topic(client: AppwriteClient, topic_id: str) -> str:
"""Delete topic."""
try:
@@ -369,6 +375,7 @@ async def delete_topic(client: AppwriteClient, topic_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_subscriber(
client: AppwriteClient, topic_id: str, subscriber_id: str, target_id: str
) -> str:
@@ -386,6 +393,7 @@ async def create_subscriber(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_subscriber(client: AppwriteClient, topic_id: str, subscriber_id: str) -> str:
"""Remove subscriber from topic."""
try:
@@ -398,6 +406,7 @@ async def delete_subscriber(client: AppwriteClient, topic_id: str, subscriber_id
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_messages(
client: AppwriteClient, queries: list[str] | None = None, search: str | None = None
) -> str:
@@ -416,6 +425,7 @@ async def list_messages(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_message(client: AppwriteClient, message_id: str) -> str:
"""Get message by ID."""
try:
@@ -425,6 +435,7 @@ async def get_message(client: AppwriteClient, message_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def send_email(
client: AppwriteClient,
message_id: str,
@@ -471,6 +482,7 @@ async def send_email(
error_msg += " (Hint: Make sure you have configured an email provider in Appwrite)"
return json.dumps({"success": False, "error": error_msg}, indent=2)
+
async def send_sms(
client: AppwriteClient,
message_id: str,
@@ -509,6 +521,7 @@ async def send_sms(
error_msg += " (Hint: Make sure you have configured an SMS provider in Appwrite)"
return json.dumps({"success": False, "error": error_msg}, indent=2)
+
async def send_push(
client: AppwriteClient,
message_id: str,
@@ -567,6 +580,7 @@ async def send_push(
)
return json.dumps({"success": False, "error": error_msg}, indent=2)
+
async def delete_message(client: AppwriteClient, message_id: str) -> str:
"""Delete message."""
try:
diff --git a/plugins/appwrite/handlers/storage.py b/plugins/appwrite/handlers/storage.py
index a2ddfe0..6b2d9e4 100644
--- a/plugins/appwrite/handlers/storage.py
+++ b/plugins/appwrite/handlers/storage.py
@@ -11,6 +11,7 @@ from typing import Any
from plugins.appwrite.client import AppwriteClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (14 tools)"""
return [
@@ -361,10 +362,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_buckets(
client: AppwriteClient, queries: list[str] | None = None, search: str | None = None
) -> str:
@@ -379,6 +382,7 @@ async def list_buckets(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_bucket(client: AppwriteClient, bucket_id: str) -> str:
"""Get bucket by ID."""
try:
@@ -388,6 +392,7 @@ async def get_bucket(client: AppwriteClient, bucket_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_bucket(
client: AppwriteClient,
bucket_id: str,
@@ -424,6 +429,7 @@ async def create_bucket(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_bucket(
client: AppwriteClient,
bucket_id: str,
@@ -452,6 +458,7 @@ async def update_bucket(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_bucket(client: AppwriteClient, bucket_id: str) -> str:
"""Delete bucket."""
try:
@@ -463,6 +470,7 @@ async def delete_bucket(client: AppwriteClient, bucket_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_files(
client: AppwriteClient,
bucket_id: str,
@@ -485,6 +493,7 @@ async def list_files(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_file(client: AppwriteClient, bucket_id: str, file_id: str) -> str:
"""Get file metadata."""
try:
@@ -494,6 +503,7 @@ async def get_file(client: AppwriteClient, bucket_id: str, file_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_file(client: AppwriteClient, bucket_id: str, file_id: str) -> str:
"""Delete file."""
try:
@@ -505,6 +515,7 @@ async def delete_file(client: AppwriteClient, bucket_id: str, file_id: str) -> s
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def download_file(client: AppwriteClient, bucket_id: str, file_id: str) -> str:
"""Download file content."""
try:
@@ -524,6 +535,7 @@ async def download_file(client: AppwriteClient, bucket_id: str, file_id: str) ->
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_file_preview(
client: AppwriteClient,
bucket_id: str,
@@ -576,6 +588,7 @@ async def get_file_preview(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_file_view(client: AppwriteClient, bucket_id: str, file_id: str) -> str:
"""Get file for viewing."""
try:
@@ -595,6 +608,7 @@ async def get_file_view(client: AppwriteClient, bucket_id: str, file_id: str) ->
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_file_url(
client: AppwriteClient, bucket_id: str, file_id: str, url_type: str = "view"
) -> str:
@@ -623,6 +637,7 @@ async def get_file_url(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def bulk_delete_files(client: AppwriteClient, bucket_id: str, file_ids: list[str]) -> str:
"""Delete multiple files."""
try:
@@ -650,6 +665,7 @@ async def bulk_delete_files(client: AppwriteClient, bucket_id: str, file_ids: li
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_bucket_stats(client: AppwriteClient, bucket_id: str) -> str:
"""Get bucket statistics."""
try:
diff --git a/plugins/appwrite/handlers/system.py b/plugins/appwrite/handlers/system.py
index 9592c1f..2d7e755 100644
--- a/plugins/appwrite/handlers/system.py
+++ b/plugins/appwrite/handlers/system.py
@@ -11,6 +11,7 @@ from typing import Any
from plugins.appwrite.client import AppwriteClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (8 tools)"""
return [
@@ -125,10 +126,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def health_check(client: AppwriteClient) -> str:
"""Comprehensive health check of all services."""
try:
@@ -147,6 +150,7 @@ async def health_check(client: AppwriteClient) -> str:
except Exception as e:
return json.dumps({"success": False, "healthy": False, "error": str(e)}, indent=2)
+
async def health_db(client: AppwriteClient) -> str:
"""Check database health."""
try:
@@ -168,6 +172,7 @@ async def health_db(client: AppwriteClient) -> str:
{"success": False, "service": "database", "status": "error", "error": str(e)}, indent=2
)
+
async def health_cache(client: AppwriteClient) -> str:
"""Check cache health."""
try:
@@ -187,6 +192,7 @@ async def health_cache(client: AppwriteClient) -> str:
{"success": False, "service": "cache", "status": "error", "error": str(e)}, indent=2
)
+
async def health_storage(client: AppwriteClient) -> str:
"""Check storage health."""
try:
@@ -208,6 +214,7 @@ async def health_storage(client: AppwriteClient) -> str:
{"success": False, "service": "storage", "status": "error", "error": str(e)}, indent=2
)
+
async def health_queue(client: AppwriteClient) -> str:
"""Check queue health."""
try:
@@ -227,6 +234,7 @@ async def health_queue(client: AppwriteClient) -> str:
{"success": False, "service": "queue", "status": "error", "error": str(e)}, indent=2
)
+
async def health_time(client: AppwriteClient) -> str:
"""Check time synchronization."""
try:
@@ -251,6 +259,7 @@ async def health_time(client: AppwriteClient) -> str:
{"success": False, "service": "time", "status": "error", "error": str(e)}, indent=2
)
+
async def get_avatar_initials(
client: AppwriteClient,
name: str | None = None,
@@ -278,6 +287,7 @@ async def get_avatar_initials(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_qr_code(client: AppwriteClient, text: str, size: int = 400, margin: int = 1) -> str:
"""Generate QR code."""
try:
diff --git a/plugins/appwrite/handlers/teams.py b/plugins/appwrite/handlers/teams.py
index 322383f..7dba503 100644
--- a/plugins/appwrite/handlers/teams.py
+++ b/plugins/appwrite/handlers/teams.py
@@ -11,6 +11,7 @@ from typing import Any
from plugins.appwrite.client import AppwriteClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (10 tools)"""
return [
@@ -202,10 +203,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_teams(
client: AppwriteClient, queries: list[str] | None = None, search: str | None = None
) -> str:
@@ -220,6 +223,7 @@ async def list_teams(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_team(client: AppwriteClient, team_id: str) -> str:
"""Get team by ID."""
try:
@@ -229,6 +233,7 @@ async def get_team(client: AppwriteClient, team_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_team(
client: AppwriteClient, team_id: str, name: str, roles: list[str] | None = None
) -> str:
@@ -244,6 +249,7 @@ async def create_team(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_team(client: AppwriteClient, team_id: str, name: str) -> str:
"""Update team name."""
try:
@@ -257,6 +263,7 @@ async def update_team(client: AppwriteClient, team_id: str, name: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_team(client: AppwriteClient, team_id: str) -> str:
"""Delete team."""
try:
@@ -268,6 +275,7 @@ async def delete_team(client: AppwriteClient, team_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_team_memberships(
client: AppwriteClient,
team_id: str,
@@ -290,6 +298,7 @@ async def list_team_memberships(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_team_membership(
client: AppwriteClient,
team_id: str,
@@ -325,6 +334,7 @@ async def create_team_membership(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_membership(
client: AppwriteClient, team_id: str, membership_id: str, roles: list[str]
) -> str:
@@ -346,6 +356,7 @@ async def update_membership(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_membership(client: AppwriteClient, team_id: str, membership_id: str) -> str:
"""Delete membership."""
try:
@@ -358,6 +369,7 @@ async def delete_membership(client: AppwriteClient, team_id: str, membership_id:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_team_prefs(client: AppwriteClient, team_id: str) -> str:
"""Get team preferences (placeholder - requires team prefs endpoint)."""
try:
diff --git a/plugins/appwrite/handlers/users.py b/plugins/appwrite/handlers/users.py
index 55ae02e..ee60f5a 100644
--- a/plugins/appwrite/handlers/users.py
+++ b/plugins/appwrite/handlers/users.py
@@ -12,6 +12,7 @@ from typing import Any
from plugins.appwrite.client import AppwriteClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (12 tools)"""
return [
@@ -220,10 +221,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_users(
client: AppwriteClient, queries: list[str] | None = None, search: str | None = None
) -> str:
@@ -238,6 +241,7 @@ async def list_users(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_user(client: AppwriteClient, user_id: str) -> str:
"""Get user by ID."""
try:
@@ -247,6 +251,7 @@ async def get_user(client: AppwriteClient, user_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_user(
client: AppwriteClient,
user_id: str,
@@ -269,6 +274,7 @@ async def create_user(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_user_name(client: AppwriteClient, user_id: str, name: str) -> str:
"""Update user name."""
try:
@@ -282,6 +288,7 @@ async def update_user_name(client: AppwriteClient, user_id: str, name: str) -> s
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_user(client: AppwriteClient, user_id: str) -> str:
"""Delete user."""
try:
@@ -293,6 +300,7 @@ async def delete_user(client: AppwriteClient, user_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_user_email(client: AppwriteClient, user_id: str, email: str) -> str:
"""Update user email."""
try:
@@ -306,6 +314,7 @@ async def update_user_email(client: AppwriteClient, user_id: str, email: str) ->
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_user_phone(client: AppwriteClient, user_id: str, number: str) -> str:
"""Update user phone."""
try:
@@ -319,6 +328,7 @@ async def update_user_phone(client: AppwriteClient, user_id: str, number: str) -
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_user_status(client: AppwriteClient, user_id: str, status: bool) -> str:
"""Update user status (enable/disable)."""
try:
@@ -333,6 +343,7 @@ async def update_user_status(client: AppwriteClient, user_id: str, status: bool)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_user_labels(client: AppwriteClient, user_id: str, labels: list[str]) -> str:
"""Update user labels."""
try:
@@ -346,6 +357,7 @@ async def update_user_labels(client: AppwriteClient, user_id: str, labels: list[
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_user_sessions(client: AppwriteClient, user_id: str) -> str:
"""List user sessions."""
try:
@@ -363,6 +375,7 @@ async def list_user_sessions(client: AppwriteClient, user_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_user_sessions(client: AppwriteClient, user_id: str) -> str:
"""Delete all user sessions."""
try:
@@ -374,6 +387,7 @@ async def delete_user_sessions(client: AppwriteClient, user_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_user_session(client: AppwriteClient, user_id: str, session_id: str) -> str:
"""Delete a specific session."""
try:
diff --git a/plugins/appwrite/plugin.py b/plugins/appwrite/plugin.py
index 59617ac..1c07c46 100644
--- a/plugins/appwrite/plugin.py
+++ b/plugins/appwrite/plugin.py
@@ -14,6 +14,7 @@ from plugins.appwrite import handlers
from plugins.appwrite.client import AppwriteClient
from plugins.base import BasePlugin
+
class AppwritePlugin(BasePlugin):
"""
Appwrite Self-Hosted Plugin - Complete backend management.
diff --git a/plugins/directus/client.py b/plugins/directus/client.py
index 77c1219..32e7bb1 100644
--- a/plugins/directus/client.py
+++ b/plugins/directus/client.py
@@ -40,6 +40,7 @@ from typing import Any
import aiohttp
+
def _ensure_list(value: Any) -> list[str]:
"""Ensure value is a list. If string, wrap in list."""
if value is None:
@@ -50,6 +51,7 @@ def _ensure_list(value: Any) -> list[str]:
return [value]
return [str(value)]
+
class DirectusClient:
"""
Directus Self-Hosted API client.
diff --git a/plugins/directus/handlers/access.py b/plugins/directus/handlers/access.py
index 2a3b2dd..78f5a3a 100644
--- a/plugins/directus/handlers/access.py
+++ b/plugins/directus/handlers/access.py
@@ -15,6 +15,7 @@ from typing import Any
from plugins.directus.client import DirectusClient
+
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
"""Parse a parameter that may be a JSON string or already a native type."""
if value is None:
@@ -30,6 +31,7 @@ def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
return value
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (12 tools)"""
return [
@@ -267,10 +269,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_roles(
client: DirectusClient,
filter: dict | None = None,
@@ -291,6 +295,7 @@ async def list_roles(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_role(client: DirectusClient, id: str) -> str:
"""Get role by ID."""
try:
@@ -301,6 +306,7 @@ async def get_role(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_role(
client: DirectusClient,
name: str,
@@ -326,6 +332,7 @@ async def create_role(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_role(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
"""Update role."""
try:
@@ -340,6 +347,7 @@ async def update_role(client: DirectusClient, id: str, data: dict[str, Any]) ->
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_role(client: DirectusClient, id: str) -> str:
"""Delete a role."""
try:
@@ -348,6 +356,7 @@ async def delete_role(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_permissions(
client: DirectusClient, filter: dict | None = None, limit: int = 100
) -> str:
@@ -365,6 +374,7 @@ async def list_permissions(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_permission(client: DirectusClient, id: str) -> str:
"""Get permission by ID."""
try:
@@ -375,6 +385,7 @@ async def get_permission(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_permission(
client: DirectusClient,
collection: str,
@@ -416,6 +427,7 @@ async def create_permission(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_permission(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
"""Update permission."""
try:
@@ -430,6 +442,7 @@ async def update_permission(client: DirectusClient, id: str, data: dict[str, Any
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_permission(client: DirectusClient, id: str) -> str:
"""Delete a permission."""
try:
@@ -438,6 +451,7 @@ async def delete_permission(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_my_permissions(client: DirectusClient) -> str:
"""Get current user's permissions."""
try:
@@ -448,6 +462,7 @@ async def get_my_permissions(client: DirectusClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_policies(
client: DirectusClient, filter: dict | None = None, limit: int = 100
) -> str:
diff --git a/plugins/directus/handlers/automation.py b/plugins/directus/handlers/automation.py
index 9d2ad7e..146a45d 100644
--- a/plugins/directus/handlers/automation.py
+++ b/plugins/directus/handlers/automation.py
@@ -12,6 +12,7 @@ from typing import Any
from plugins.directus.client import DirectusClient
+
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
"""Parse a parameter that may be a JSON string or already a native type."""
if value is None:
@@ -27,6 +28,7 @@ def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
return value
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (12 tools)"""
return [
@@ -303,10 +305,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_flows(
client: DirectusClient,
filter: dict | None = None,
@@ -327,6 +331,7 @@ async def list_flows(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_flow(client: DirectusClient, id: str) -> str:
"""Get flow by ID."""
try:
@@ -337,6 +342,7 @@ async def get_flow(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_flow(
client: DirectusClient,
name: str,
@@ -369,6 +375,7 @@ async def create_flow(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_flow(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
"""Update flow."""
try:
@@ -383,6 +390,7 @@ async def update_flow(client: DirectusClient, id: str, data: dict[str, Any]) ->
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_flow(client: DirectusClient, id: str) -> str:
"""Delete a flow."""
try:
@@ -391,6 +399,7 @@ async def delete_flow(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def trigger_flow(client: DirectusClient, id: str, data: dict | None = None) -> str:
"""Trigger a flow manually."""
try:
@@ -409,6 +418,7 @@ async def trigger_flow(client: DirectusClient, id: str, data: dict | None = None
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_operations(
client: DirectusClient, filter: dict | None = None, limit: int = 100
) -> str:
@@ -426,6 +436,7 @@ async def list_operations(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_operation(
client: DirectusClient,
flow: str,
@@ -462,6 +473,7 @@ async def create_operation(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_webhooks(
client: DirectusClient, filter: dict | None = None, limit: int = 100
) -> str:
@@ -484,6 +496,7 @@ async def list_webhooks(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_webhook(
client: DirectusClient,
name: str,
@@ -531,6 +544,7 @@ async def create_webhook(
indent=2,
)
+
async def update_webhook(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
"""Update webhook. DEPRECATED: Use Flows instead in Directus 10+."""
try:
@@ -557,6 +571,7 @@ async def update_webhook(client: DirectusClient, id: str, data: dict[str, Any])
indent=2,
)
+
async def delete_webhook(client: DirectusClient, id: str) -> str:
"""Delete a webhook. DEPRECATED: Use Flows instead in Directus 10+."""
try:
diff --git a/plugins/directus/handlers/collections.py b/plugins/directus/handlers/collections.py
index f80fe89..7611698 100644
--- a/plugins/directus/handlers/collections.py
+++ b/plugins/directus/handlers/collections.py
@@ -12,6 +12,7 @@ from typing import Any
from plugins.directus.client import DirectusClient
+
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
"""
Parse a parameter that may be a JSON string or already a native type.
@@ -45,6 +46,7 @@ def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
return value
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (14 tools)"""
return [
@@ -294,10 +296,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_collections(client: DirectusClient) -> str:
"""List all collections."""
try:
@@ -311,6 +315,7 @@ async def list_collections(client: DirectusClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_collection(client: DirectusClient, collection: str) -> str:
"""Get collection details."""
try:
@@ -321,6 +326,7 @@ async def get_collection(client: DirectusClient, collection: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_collection(
client: DirectusClient,
collection: str,
@@ -358,6 +364,7 @@ async def create_collection(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_collection(client: DirectusClient, collection: str, meta: dict) -> str:
"""Update collection meta."""
try:
@@ -376,6 +383,7 @@ async def update_collection(client: DirectusClient, collection: str, meta: dict)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_collection(client: DirectusClient, collection: str) -> str:
"""Delete a collection."""
try:
@@ -386,6 +394,7 @@ async def delete_collection(client: DirectusClient, collection: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_fields(client: DirectusClient, collection: str | None = None) -> str:
"""List fields."""
try:
@@ -399,6 +408,7 @@ async def list_fields(client: DirectusClient, collection: str | None = None) ->
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_field(client: DirectusClient, collection: str, field: str) -> str:
"""Get field details."""
try:
@@ -409,6 +419,7 @@ async def get_field(client: DirectusClient, collection: str, field: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_field(
client: DirectusClient,
collection: str,
@@ -438,6 +449,7 @@ async def create_field(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_field(
client: DirectusClient,
collection: str,
@@ -462,6 +474,7 @@ async def update_field(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_field(client: DirectusClient, collection: str, field: str) -> str:
"""Delete a field."""
try:
@@ -472,6 +485,7 @@ async def delete_field(client: DirectusClient, collection: str, field: str) -> s
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_relations(client: DirectusClient, collection: str | None = None) -> str:
"""List relations."""
try:
@@ -490,6 +504,7 @@ async def list_relations(client: DirectusClient, collection: str | None = None)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_relation(client: DirectusClient, collection: str, field: str) -> str:
"""Get relation details."""
try:
@@ -500,6 +515,7 @@ async def get_relation(client: DirectusClient, collection: str, field: str) -> s
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_relation(
client: DirectusClient,
collection: str,
@@ -533,6 +549,7 @@ async def create_relation(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_relation(client: DirectusClient, collection: str, field: str) -> str:
"""Delete a relation."""
try:
diff --git a/plugins/directus/handlers/content.py b/plugins/directus/handlers/content.py
index de70982..7d71fd2 100644
--- a/plugins/directus/handlers/content.py
+++ b/plugins/directus/handlers/content.py
@@ -12,6 +12,7 @@ from typing import Any
from plugins.directus.client import DirectusClient
+
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
"""Parse a parameter that may be a JSON string or already a native type."""
if value is None:
@@ -27,6 +28,7 @@ def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
return value
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (10 tools)"""
return [
@@ -211,10 +213,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_revisions(
client: DirectusClient,
filter: dict | None = None,
@@ -237,6 +241,7 @@ async def list_revisions(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_revision(client: DirectusClient, id: str) -> str:
"""Get revision by ID."""
try:
@@ -247,6 +252,7 @@ async def get_revision(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_versions(
client: DirectusClient,
filter: dict | None = None,
@@ -269,6 +275,7 @@ async def list_versions(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_version(client: DirectusClient, id: str) -> str:
"""Get version by ID."""
try:
@@ -279,6 +286,7 @@ async def get_version(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_version(
client: DirectusClient, name: str, collection: str, item: str, key: str
) -> str:
@@ -297,6 +305,7 @@ async def create_version(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_version(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
"""Update version."""
try:
@@ -311,6 +320,7 @@ async def update_version(client: DirectusClient, id: str, data: dict[str, Any])
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_version(client: DirectusClient, id: str) -> str:
"""Delete a version."""
try:
@@ -319,6 +329,7 @@ async def delete_version(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def promote_version(client: DirectusClient, id: str, mainHash: str | None = None) -> str:
"""Promote version to main."""
try:
@@ -335,6 +346,7 @@ async def promote_version(client: DirectusClient, id: str, mainHash: str | None
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_comments(
client: DirectusClient,
filter: dict | None = None,
@@ -357,6 +369,7 @@ async def list_comments(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_comment(client: DirectusClient, collection: str, item: str, comment: str) -> str:
"""Create a comment."""
try:
diff --git a/plugins/directus/handlers/dashboards.py b/plugins/directus/handlers/dashboards.py
index 122e988..875fe29 100644
--- a/plugins/directus/handlers/dashboards.py
+++ b/plugins/directus/handlers/dashboards.py
@@ -11,6 +11,7 @@ from typing import Any
from plugins.directus.client import DirectusClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (8 tools)"""
return [
@@ -201,10 +202,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_dashboards(
client: DirectusClient,
filter: dict | None = None,
@@ -223,6 +226,7 @@ async def list_dashboards(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_dashboard(client: DirectusClient, id: str) -> str:
"""Get dashboard by ID."""
try:
@@ -233,6 +237,7 @@ async def get_dashboard(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_dashboard(
client: DirectusClient,
name: str,
@@ -255,6 +260,7 @@ async def create_dashboard(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_dashboard(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
"""Update dashboard."""
try:
@@ -267,6 +273,7 @@ async def update_dashboard(client: DirectusClient, id: str, data: dict[str, Any]
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_dashboard(client: DirectusClient, id: str) -> str:
"""Delete a dashboard."""
try:
@@ -275,6 +282,7 @@ async def delete_dashboard(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_panels(client: DirectusClient, filter: dict | None = None, limit: int = 100) -> str:
"""List panels."""
try:
@@ -286,6 +294,7 @@ async def list_panels(client: DirectusClient, filter: dict | None = None, limit:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_panel(
client: DirectusClient,
dashboard: str,
@@ -323,6 +332,7 @@ async def create_panel(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_panel(client: DirectusClient, id: str) -> str:
"""Delete a panel."""
try:
diff --git a/plugins/directus/handlers/files.py b/plugins/directus/handlers/files.py
index 4744136..7df2352 100644
--- a/plugins/directus/handlers/files.py
+++ b/plugins/directus/handlers/files.py
@@ -12,6 +12,7 @@ from typing import Any
from plugins.directus.client import DirectusClient
+
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
"""Parse a parameter that may be a JSON string or already a native type."""
if value is None:
@@ -27,6 +28,7 @@ def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
return value
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (12 tools)"""
return [
@@ -241,10 +243,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_files(
client: DirectusClient,
filter: dict | None = None,
@@ -269,6 +273,7 @@ async def list_files(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_file(client: DirectusClient, id: str) -> str:
"""Get file metadata."""
try:
@@ -279,6 +284,7 @@ async def get_file(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_file(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
"""Update file metadata."""
try:
@@ -293,6 +299,7 @@ async def update_file(client: DirectusClient, id: str, data: dict[str, Any]) ->
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_file(client: DirectusClient, id: str) -> str:
"""Delete a file."""
try:
@@ -301,6 +308,7 @@ async def delete_file(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_files(client: DirectusClient, ids: list[str]) -> str:
"""Delete multiple files."""
try:
@@ -311,6 +319,7 @@ async def delete_files(client: DirectusClient, ids: list[str]) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def import_file_url(client: DirectusClient, url: str, data: dict | None = None) -> str:
"""Import file from URL."""
try:
@@ -325,6 +334,7 @@ async def import_file_url(client: DirectusClient, url: str, data: dict | None =
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_file_url(client: DirectusClient, id: str, download: bool = False) -> str:
"""Get file URL."""
try:
@@ -336,6 +346,7 @@ async def get_file_url(client: DirectusClient, id: str, download: bool = False)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_folders(
client: DirectusClient,
filter: dict | None = None,
@@ -361,6 +372,7 @@ async def list_folders(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_folder(client: DirectusClient, id: str) -> str:
"""Get folder details."""
try:
@@ -371,6 +383,7 @@ async def get_folder(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_folder(client: DirectusClient, name: str, parent: str | None = None) -> str:
"""Create a folder."""
try:
@@ -383,6 +396,7 @@ async def create_folder(client: DirectusClient, name: str, parent: str | None =
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_folder(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
"""Update folder."""
try:
@@ -397,6 +411,7 @@ async def update_folder(client: DirectusClient, id: str, data: dict[str, Any]) -
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_folder(client: DirectusClient, id: str) -> str:
"""Delete a folder."""
try:
diff --git a/plugins/directus/handlers/items.py b/plugins/directus/handlers/items.py
index 23ce0e8..b61637b 100644
--- a/plugins/directus/handlers/items.py
+++ b/plugins/directus/handlers/items.py
@@ -12,6 +12,7 @@ from typing import Any
from plugins.directus.client import DirectusClient
+
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
"""Parse a parameter that may be a JSON string or already a native type."""
if value is None:
@@ -27,6 +28,7 @@ def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
return value
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (12 tools)"""
return [
@@ -301,10 +303,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_items(
client: DirectusClient,
collection: str,
@@ -340,6 +344,7 @@ async def list_items(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_item(
client: DirectusClient, collection: str, id: str, fields: list[str] | None = None
) -> str:
@@ -354,6 +359,7 @@ async def get_item(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_item(
client: DirectusClient, collection: str, data: dict[str, Any], fields: list[str] | None = None
) -> str:
@@ -375,6 +381,7 @@ async def create_item(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_items(
client: DirectusClient,
collection: str,
@@ -400,6 +407,7 @@ async def create_items(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_item(
client: DirectusClient,
collection: str,
@@ -421,6 +429,7 @@ async def update_item(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_items(
client: DirectusClient,
collection: str,
@@ -450,6 +459,7 @@ async def update_items(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_item(client: DirectusClient, collection: str, id: str) -> str:
"""Delete an item."""
try:
@@ -460,6 +470,7 @@ async def delete_item(client: DirectusClient, collection: str, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_items(client: DirectusClient, collection: str, keys: list[str]) -> str:
"""Delete multiple items."""
try:
@@ -472,6 +483,7 @@ async def delete_items(client: DirectusClient, collection: str, keys: list[str])
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def search_items(
client: DirectusClient,
collection: str,
@@ -495,6 +507,7 @@ async def search_items(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def aggregate_items(
client: DirectusClient,
collection: str,
@@ -520,6 +533,7 @@ async def aggregate_items(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def export_items(
client: DirectusClient,
collection: str,
@@ -556,6 +570,7 @@ async def export_items(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def import_items(client: DirectusClient, collection: str, data: list[dict[str, Any]]) -> str:
"""Import items into a collection."""
try:
diff --git a/plugins/directus/handlers/system.py b/plugins/directus/handlers/system.py
index ebea452..f2bd751 100644
--- a/plugins/directus/handlers/system.py
+++ b/plugins/directus/handlers/system.py
@@ -13,6 +13,7 @@ from typing import Any
from plugins.directus.client import DirectusClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (10 tools)"""
return [
@@ -135,10 +136,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def get_settings(client: DirectusClient) -> str:
"""Get system settings."""
try:
@@ -149,6 +152,7 @@ async def get_settings(client: DirectusClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_settings(client: DirectusClient, data: dict[str, Any]) -> str:
"""Update system settings."""
try:
@@ -161,6 +165,7 @@ async def update_settings(client: DirectusClient, data: dict[str, Any]) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_server_info(client: DirectusClient) -> str:
"""Get server info."""
try:
@@ -171,6 +176,7 @@ async def get_server_info(client: DirectusClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def health_check(client: DirectusClient) -> str:
"""Check server health."""
try:
@@ -183,6 +189,7 @@ async def health_check(client: DirectusClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_graphql_sdl(client: DirectusClient) -> str:
"""Get GraphQL SDL."""
try:
@@ -196,6 +203,7 @@ async def get_graphql_sdl(client: DirectusClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_openapi_spec(client: DirectusClient) -> str:
"""Get OpenAPI specification."""
try:
@@ -204,6 +212,7 @@ async def get_openapi_spec(client: DirectusClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_schema_snapshot(client: DirectusClient) -> str:
"""Get schema snapshot."""
try:
@@ -214,6 +223,7 @@ async def get_schema_snapshot(client: DirectusClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def schema_diff(client: DirectusClient, snapshot: dict[str, Any]) -> str:
"""Get schema diff."""
try:
@@ -224,6 +234,7 @@ async def schema_diff(client: DirectusClient, snapshot: dict[str, Any]) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def schema_apply(client: DirectusClient, diff: dict[str, Any]) -> str:
"""Apply schema diff."""
try:
@@ -240,6 +251,7 @@ async def schema_apply(client: DirectusClient, diff: dict[str, Any]) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def list_activity(
client: DirectusClient,
filter: dict | None = None,
diff --git a/plugins/directus/handlers/users.py b/plugins/directus/handlers/users.py
index dbd6018..032187a 100644
--- a/plugins/directus/handlers/users.py
+++ b/plugins/directus/handlers/users.py
@@ -12,6 +12,7 @@ from typing import Any
from plugins.directus.client import DirectusClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (10 tools)"""
return [
@@ -192,10 +193,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# HANDLER FUNCTIONS
# =====================
+
async def list_users(
client: DirectusClient,
filter: dict | None = None,
@@ -216,6 +219,7 @@ async def list_users(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_user(client: DirectusClient, id: str) -> str:
"""Get user by ID."""
try:
@@ -226,6 +230,7 @@ async def get_user(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_current_user(client: DirectusClient) -> str:
"""Get current user."""
try:
@@ -236,6 +241,7 @@ async def get_current_user(client: DirectusClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_user(
client: DirectusClient,
email: str,
@@ -263,6 +269,7 @@ async def create_user(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_user(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
"""Update user."""
try:
@@ -275,6 +282,7 @@ async def update_user(client: DirectusClient, id: str, data: dict[str, Any]) ->
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_user(client: DirectusClient, id: str) -> str:
"""Delete a user."""
try:
@@ -283,6 +291,7 @@ async def delete_user(client: DirectusClient, id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_users(client: DirectusClient, ids: list[str]) -> str:
"""Delete multiple users."""
try:
@@ -291,6 +300,7 @@ async def delete_users(client: DirectusClient, ids: list[str]) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def invite_user(
client: DirectusClient, email: str, role: str, invite_url: str | None = None
) -> str:
@@ -309,6 +319,7 @@ async def invite_user(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_current_user(client: DirectusClient, data: dict[str, Any]) -> str:
"""Update current user profile."""
try:
@@ -329,6 +340,7 @@ async def update_current_user(client: DirectusClient, data: dict[str, Any]) -> s
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_user_role(client: DirectusClient, id: str) -> str:
"""Get user's role."""
try:
diff --git a/plugins/directus/plugin.py b/plugins/directus/plugin.py
index a25d59e..0f4099c 100644
--- a/plugins/directus/plugin.py
+++ b/plugins/directus/plugin.py
@@ -14,6 +14,7 @@ from plugins.base import BasePlugin
from plugins.directus import handlers
from plugins.directus.client import DirectusClient
+
class DirectusPlugin(BasePlugin):
"""
Directus Self-Hosted Plugin - Complete CMS management.
diff --git a/plugins/gitea/client.py b/plugins/gitea/client.py
index 9006d96..b93be58 100644
--- a/plugins/gitea/client.py
+++ b/plugins/gitea/client.py
@@ -11,6 +11,7 @@ from typing import Any
import aiohttp
+
class GiteaClient:
"""
Gitea REST API client for HTTP communication.
diff --git a/plugins/gitea/handlers/issues.py b/plugins/gitea/handlers/issues.py
index 4f46862..8bfffe2 100644
--- a/plugins/gitea/handlers/issues.py
+++ b/plugins/gitea/handlers/issues.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.gitea.client import GiteaClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -353,6 +354,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def list_issues(
client: GiteaClient,
owner: str,
@@ -369,12 +371,14 @@ async def list_issues(
result = {"success": True, "count": len(issues), "issues": issues}
return json.dumps(result, indent=2)
+
async def get_issue(client: GiteaClient, owner: str, repo: str, issue_number: int) -> str:
"""Get issue details"""
issue = await client.get_issue(owner, repo, issue_number)
result = {"success": True, "issue": issue}
return json.dumps(result, indent=2)
+
async def create_issue(
client: GiteaClient,
owner: str,
@@ -405,6 +409,7 @@ async def create_issue(
}
return json.dumps(result, indent=2)
+
async def update_issue(
client: GiteaClient, owner: str, repo: str, issue_number: int, **kwargs
) -> str:
@@ -424,6 +429,7 @@ async def update_issue(
}
return json.dumps(result, indent=2)
+
async def close_issue(client: GiteaClient, owner: str, repo: str, issue_number: int) -> str:
"""Close an issue"""
data = {"state": "closed"}
@@ -435,6 +441,7 @@ async def close_issue(client: GiteaClient, owner: str, repo: str, issue_number:
}
return json.dumps(result, indent=2)
+
async def reopen_issue(client: GiteaClient, owner: str, repo: str, issue_number: int) -> str:
"""Reopen an issue"""
data = {"state": "open"}
@@ -446,6 +453,7 @@ async def reopen_issue(client: GiteaClient, owner: str, repo: str, issue_number:
}
return json.dumps(result, indent=2)
+
# Comment operations
async def list_issue_comments(client: GiteaClient, owner: str, repo: str, issue_number: int) -> str:
"""List issue comments"""
@@ -453,6 +461,7 @@ async def list_issue_comments(client: GiteaClient, owner: str, repo: str, issue_
result = {"success": True, "count": len(comments), "comments": comments}
return json.dumps(result, indent=2)
+
async def create_issue_comment(
client: GiteaClient, owner: str, repo: str, issue_number: int, body: str
) -> str:
@@ -466,6 +475,7 @@ async def create_issue_comment(
}
return json.dumps(result, indent=2)
+
# Label operations
async def list_labels(client: GiteaClient, owner: str, repo: str) -> str:
"""List repository labels"""
@@ -473,6 +483,7 @@ async def list_labels(client: GiteaClient, owner: str, repo: str) -> str:
result = {"success": True, "count": len(labels), "labels": labels}
return json.dumps(result, indent=2)
+
async def create_label(
client: GiteaClient,
owner: str,
@@ -487,6 +498,7 @@ async def create_label(
result = {"success": True, "message": f"Label '{name}' created successfully", "label": label}
return json.dumps(result, indent=2)
+
# Milestone operations
async def list_milestones(
client: GiteaClient, owner: str, repo: str, state: str | None = None
@@ -496,6 +508,7 @@ async def list_milestones(
result = {"success": True, "count": len(milestones), "milestones": milestones}
return json.dumps(result, indent=2)
+
async def create_milestone(
client: GiteaClient,
owner: str,
diff --git a/plugins/gitea/handlers/pull_requests.py b/plugins/gitea/handlers/pull_requests.py
index ef8b650..7e88f14 100644
--- a/plugins/gitea/handlers/pull_requests.py
+++ b/plugins/gitea/handlers/pull_requests.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.gitea.client import GiteaClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -430,6 +431,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def list_pull_requests(
client: GiteaClient,
owner: str,
@@ -454,12 +456,14 @@ async def list_pull_requests(
result = {"success": True, "count": len(prs), "pull_requests": prs}
return json.dumps(result, indent=2)
+
async def get_pull_request(client: GiteaClient, owner: str, repo: str, pr_number: int) -> str:
"""Get pull request details"""
pr = await client.get_pull_request(owner, repo, pr_number)
result = {"success": True, "pull_request": pr}
return json.dumps(result, indent=2)
+
async def create_pull_request(
client: GiteaClient,
owner: str,
@@ -492,6 +496,7 @@ async def create_pull_request(
}
return json.dumps(result, indent=2)
+
async def update_pull_request(
client: GiteaClient, owner: str, repo: str, pr_number: int, **kwargs
) -> str:
@@ -509,6 +514,7 @@ async def update_pull_request(
}
return json.dumps(result, indent=2)
+
async def merge_pull_request(
client: GiteaClient,
owner: str,
@@ -534,6 +540,7 @@ async def merge_pull_request(
}
return json.dumps(result, indent=2)
+
async def close_pull_request(client: GiteaClient, owner: str, repo: str, pr_number: int) -> str:
"""Close a pull request"""
data = {"state": "closed"}
@@ -545,6 +552,7 @@ async def close_pull_request(client: GiteaClient, owner: str, repo: str, pr_numb
}
return json.dumps(result, indent=2)
+
async def reopen_pull_request(client: GiteaClient, owner: str, repo: str, pr_number: int) -> str:
"""Reopen a pull request"""
data = {"state": "open"}
@@ -556,6 +564,7 @@ async def reopen_pull_request(client: GiteaClient, owner: str, repo: str, pr_num
}
return json.dumps(result, indent=2)
+
# PR Details
async def list_pr_commits(client: GiteaClient, owner: str, repo: str, pr_number: int) -> str:
"""List pull request commits"""
@@ -563,18 +572,21 @@ async def list_pr_commits(client: GiteaClient, owner: str, repo: str, pr_number:
result = {"success": True, "count": len(commits), "commits": commits}
return json.dumps(result, indent=2)
+
async def list_pr_files(client: GiteaClient, owner: str, repo: str, pr_number: int) -> str:
"""List pull request files"""
files = await client.list_pr_files(owner, repo, pr_number)
result = {"success": True, "count": len(files), "files": files}
return json.dumps(result, indent=2)
+
async def get_pr_diff(client: GiteaClient, owner: str, repo: str, pr_number: int) -> str:
"""Get pull request diff"""
diff = await client.get_pr_diff(owner, repo, pr_number)
result = {"success": True, "diff": diff}
return json.dumps(result, indent=2)
+
async def list_pr_comments(client: GiteaClient, owner: str, repo: str, pr_number: int) -> str:
"""List pull request comments"""
# PR comments are same as issue comments in Gitea API
@@ -582,6 +594,7 @@ async def list_pr_comments(client: GiteaClient, owner: str, repo: str, pr_number
result = {"success": True, "count": len(comments), "comments": comments}
return json.dumps(result, indent=2)
+
async def create_pr_comment(
client: GiteaClient, owner: str, repo: str, pr_number: int, body: str
) -> str:
@@ -595,6 +608,7 @@ async def create_pr_comment(
}
return json.dumps(result, indent=2)
+
# PR Reviews
async def list_pr_reviews(client: GiteaClient, owner: str, repo: str, pr_number: int) -> str:
"""List pull request reviews"""
@@ -602,6 +616,7 @@ async def list_pr_reviews(client: GiteaClient, owner: str, repo: str, pr_number:
result = {"success": True, "count": len(reviews), "reviews": reviews}
return json.dumps(result, indent=2)
+
async def create_pr_review(
client: GiteaClient, owner: str, repo: str, pr_number: int, event: str, body: str | None = None
) -> str:
@@ -615,6 +630,7 @@ async def create_pr_review(
}
return json.dumps(result, indent=2)
+
async def request_pr_reviewers(
client: GiteaClient, owner: str, repo: str, pr_number: int, reviewers: list[str]
) -> str:
diff --git a/plugins/gitea/handlers/repositories.py b/plugins/gitea/handlers/repositories.py
index e8c28ee..5eb657a 100644
--- a/plugins/gitea/handlers/repositories.py
+++ b/plugins/gitea/handlers/repositories.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.gitea.client import GiteaClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -483,6 +484,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def list_repositories(
client: GiteaClient, owner: str | None = None, type: str = "all", page: int = 1, limit: int = 30
) -> str:
@@ -491,12 +493,14 @@ async def list_repositories(
result = {"success": True, "count": len(repos), "repositories": repos}
return json.dumps(result, indent=2)
+
async def get_repository(client: GiteaClient, owner: str, repo: str) -> str:
"""Get repository details"""
repository = await client.get_repository(owner, repo)
result = {"success": True, "repository": repository}
return json.dumps(result, indent=2)
+
async def create_repository(
client: GiteaClient,
name: str,
@@ -528,6 +532,7 @@ async def create_repository(
}
return json.dumps(result, indent=2)
+
async def update_repository(client: GiteaClient, owner: str, repo: str, **kwargs) -> str:
"""Update repository settings"""
# Build update data from kwargs
@@ -541,12 +546,14 @@ async def update_repository(client: GiteaClient, owner: str, repo: str, **kwargs
}
return json.dumps(result, indent=2)
+
async def delete_repository(client: GiteaClient, owner: str, repo: str) -> str:
"""Delete a repository"""
await client.delete_repository(owner, repo)
result = {"success": True, "message": f"Repository '{owner}/{repo}' deleted successfully"}
return json.dumps(result, indent=2)
+
# Branch operations
async def list_branches(
client: GiteaClient, owner: str, repo: str, page: int = 1, limit: int = 30
@@ -556,12 +563,14 @@ async def list_branches(
result = {"success": True, "count": len(branches), "branches": branches}
return json.dumps(result, indent=2)
+
async def get_branch(client: GiteaClient, owner: str, repo: str, branch: str) -> str:
"""Get branch details"""
branch_info = await client.get_branch(owner, repo, branch)
result = {"success": True, "branch": branch_info}
return json.dumps(result, indent=2)
+
async def create_branch(
client: GiteaClient,
owner: str,
@@ -584,12 +593,14 @@ async def create_branch(
}
return json.dumps(result, indent=2)
+
async def delete_branch(client: GiteaClient, owner: str, repo: str, branch: str) -> str:
"""Delete a branch"""
await client.delete_branch(owner, repo, branch)
result = {"success": True, "message": f"Branch '{branch}' deleted successfully"}
return json.dumps(result, indent=2)
+
# Tag operations
async def list_tags(
client: GiteaClient, owner: str, repo: str, page: int = 1, limit: int = 30
@@ -599,6 +610,7 @@ async def list_tags(
result = {"success": True, "count": len(tags), "tags": tags}
return json.dumps(result, indent=2)
+
async def create_tag(
client: GiteaClient,
owner: str,
@@ -613,12 +625,14 @@ async def create_tag(
result = {"success": True, "message": f"Tag '{tag_name}' created successfully", "tag": tag}
return json.dumps(result, indent=2)
+
async def delete_tag(client: GiteaClient, owner: str, repo: str, tag: str) -> str:
"""Delete a tag"""
await client.delete_tag(owner, repo, tag)
result = {"success": True, "message": f"Tag '{tag}' deleted successfully"}
return json.dumps(result, indent=2)
+
# File operations
async def get_file(
client: GiteaClient, owner: str, repo: str, path: str, ref: str | None = None
@@ -628,6 +642,7 @@ async def get_file(
result = {"success": True, "file": file_data}
return json.dumps(result, indent=2)
+
async def create_file(
client: GiteaClient,
owner: str,
@@ -664,6 +679,7 @@ async def create_file(
}
return json.dumps(result, indent=2)
+
async def update_file(
client: GiteaClient,
owner: str,
@@ -694,6 +710,7 @@ async def update_file(
}
return json.dumps(result, indent=2)
+
async def delete_file(
client: GiteaClient,
owner: str,
diff --git a/plugins/gitea/handlers/users.py b/plugins/gitea/handlers/users.py
index 3de83fa..5919268 100644
--- a/plugins/gitea/handlers/users.py
+++ b/plugins/gitea/handlers/users.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.gitea.client import GiteaClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -190,12 +191,14 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def get_user(client: GiteaClient, username: str) -> str:
"""Get user information"""
user = await client.get_user(username)
result = {"success": True, "user": user}
return json.dumps(result, indent=2)
+
async def list_user_repos(
client: GiteaClient, username: str, page: int = 1, limit: int = 30
) -> str:
@@ -204,12 +207,14 @@ async def list_user_repos(
result = {"success": True, "count": len(repos), "repositories": repos}
return json.dumps(result, indent=2)
+
async def search_users(client: GiteaClient, q: str | None = None, uid: int | None = None) -> str:
"""Search users"""
users = await client.search_users(query=q, uid=uid)
result = {"success": True, "count": len(users), "users": users}
return json.dumps(result, indent=2)
+
# Organization operations
async def list_organizations(client: GiteaClient, page: int = 1, limit: int = 30) -> str:
"""List organizations"""
@@ -217,18 +222,21 @@ async def list_organizations(client: GiteaClient, page: int = 1, limit: int = 30
result = {"success": True, "count": len(orgs), "organizations": orgs}
return json.dumps(result, indent=2)
+
async def get_organization(client: GiteaClient, org: str) -> str:
"""Get organization information"""
organization = await client.get_organization(org)
result = {"success": True, "organization": organization}
return json.dumps(result, indent=2)
+
async def list_org_repos(client: GiteaClient, org: str, page: int = 1, limit: int = 30) -> str:
"""List organization repositories"""
repos = await client.list_org_repos(org, page=page, limit=limit)
result = {"success": True, "count": len(repos), "repositories": repos}
return json.dumps(result, indent=2)
+
# Team operations
async def list_org_teams(client: GiteaClient, org: str, page: int = 1, limit: int = 30) -> str:
"""List organization teams"""
@@ -236,6 +244,7 @@ async def list_org_teams(client: GiteaClient, org: str, page: int = 1, limit: in
result = {"success": True, "count": len(teams), "teams": teams}
return json.dumps(result, indent=2)
+
async def list_team_members(
client: GiteaClient, team_id: int, page: int = 1, limit: int = 30
) -> str:
diff --git a/plugins/gitea/handlers/webhooks.py b/plugins/gitea/handlers/webhooks.py
index 80b13c6..9f37d0e 100644
--- a/plugins/gitea/handlers/webhooks.py
+++ b/plugins/gitea/handlers/webhooks.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.gitea.client import GiteaClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -160,12 +161,14 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def list_webhooks(client: GiteaClient, owner: str, repo: str) -> str:
"""List repository webhooks"""
webhooks = await client.list_webhooks(owner, repo)
result = {"success": True, "count": len(webhooks), "webhooks": webhooks}
return json.dumps(result, indent=2)
+
async def create_webhook(
client: GiteaClient,
owner: str,
@@ -193,12 +196,14 @@ async def create_webhook(
}
return json.dumps(result, indent=2)
+
async def delete_webhook(client: GiteaClient, owner: str, repo: str, webhook_id: int) -> str:
"""Delete a webhook"""
await client.delete_webhook(owner, repo, webhook_id)
result = {"success": True, "message": f"Webhook {webhook_id} deleted successfully"}
return json.dumps(result, indent=2)
+
async def test_webhook(client: GiteaClient, owner: str, repo: str, webhook_id: int) -> str:
"""Test a webhook"""
test_result = await client.test_webhook(owner, repo, webhook_id)
@@ -209,6 +214,7 @@ async def test_webhook(client: GiteaClient, owner: str, repo: str, webhook_id: i
}
return json.dumps(result, indent=2)
+
async def get_webhook(client: GiteaClient, owner: str, repo: str, webhook_id: int) -> str:
"""Get webhook details"""
webhook = await client.get_webhook(owner, repo, webhook_id)
diff --git a/plugins/gitea/plugin.py b/plugins/gitea/plugin.py
index a133879..c227418 100644
--- a/plugins/gitea/plugin.py
+++ b/plugins/gitea/plugin.py
@@ -11,6 +11,7 @@ from plugins.base import BasePlugin
from plugins.gitea import handlers
from plugins.gitea.client import GiteaClient
+
class GiteaPlugin(BasePlugin):
"""
Gitea project plugin - Clean architecture.
diff --git a/plugins/gitea/schemas/common.py b/plugins/gitea/schemas/common.py
index 55497b7..9d07536 100644
--- a/plugins/gitea/schemas/common.py
+++ b/plugins/gitea/schemas/common.py
@@ -9,6 +9,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field
+
class Site(BaseModel):
"""Gitea site configuration"""
@@ -20,6 +21,7 @@ class Site(BaseModel):
alias: str | None = Field(None, description="Site alias")
oauth_enabled: bool = Field(default=False, description="Whether OAuth is enabled")
+
class PaginationParams(BaseModel):
"""Pagination parameters for list endpoints"""
@@ -28,6 +30,7 @@ class PaginationParams(BaseModel):
page: int = Field(default=1, ge=1, description="Page number (starts at 1)")
limit: int = Field(default=30, ge=1, le=100, description="Number of items per page (1-100)")
+
class ErrorResponse(BaseModel):
"""Standard error response"""
@@ -36,6 +39,7 @@ class ErrorResponse(BaseModel):
code: str | None = Field(None, description="Error code")
details: dict[str, Any] | None = Field(None, description="Additional error details")
+
class SuccessResponse(BaseModel):
"""Standard success response"""
@@ -43,6 +47,7 @@ class SuccessResponse(BaseModel):
message: str = Field(..., description="Success message")
data: dict[str, Any] | None = Field(None, description="Response data")
+
class GiteaUser(BaseModel):
"""Gitea user information"""
@@ -55,6 +60,7 @@ class GiteaUser(BaseModel):
avatar_url: str | None = None
is_admin: bool = False
+
class GiteaPermissions(BaseModel):
"""Repository permissions"""
@@ -64,6 +70,7 @@ class GiteaPermissions(BaseModel):
push: bool = False
pull: bool = False
+
class GiteaTimestamps(BaseModel):
"""Common timestamp fields"""
diff --git a/plugins/gitea/schemas/issue.py b/plugins/gitea/schemas/issue.py
index c62f65a..3464e05 100644
--- a/plugins/gitea/schemas/issue.py
+++ b/plugins/gitea/schemas/issue.py
@@ -10,6 +10,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
from .common import GiteaUser
+
class Label(BaseModel):
"""Issue/PR label"""
@@ -21,6 +22,7 @@ class Label(BaseModel):
description: str | None = None
url: str | None = None
+
class Milestone(BaseModel):
"""Issue/PR milestone"""
@@ -37,6 +39,7 @@ class Milestone(BaseModel):
closed_at: datetime | None = None
due_on: datetime | None = None
+
class Issue(BaseModel):
"""Gitea issue model"""
@@ -66,6 +69,7 @@ class Issue(BaseModel):
html_url: str
url: str
+
class Comment(BaseModel):
"""Issue/PR comment"""
@@ -82,6 +86,7 @@ class Comment(BaseModel):
created_at: datetime
updated_at: datetime
+
class CreateIssueRequest(BaseModel):
"""Request to create an issue"""
@@ -97,6 +102,7 @@ class CreateIssueRequest(BaseModel):
milestone: int | None = Field(None, description="Milestone ID")
ref: str | None = Field(None, description="Issue ref")
+
class UpdateIssueRequest(BaseModel):
"""Request to update an issue"""
@@ -120,6 +126,7 @@ class UpdateIssueRequest(BaseModel):
raise ValueError("State must be 'open' or 'closed'")
return v
+
class CreateCommentRequest(BaseModel):
"""Request to create a comment"""
@@ -127,6 +134,7 @@ class CreateCommentRequest(BaseModel):
body: str = Field(..., min_length=1, description="Comment body")
+
class CreateLabelRequest(BaseModel):
"""Request to create a label"""
@@ -136,6 +144,7 @@ class CreateLabelRequest(BaseModel):
color: str = Field(..., pattern=r"^[0-9A-Fa-f]{6}$", description="Label color (hex without #)")
description: str | None = Field(None, max_length=200, description="Label description")
+
class CreateMilestoneRequest(BaseModel):
"""Request to create a milestone"""
@@ -153,6 +162,7 @@ class CreateMilestoneRequest(BaseModel):
raise ValueError("State must be 'open' or 'closed'")
return v
+
class IssueListFilters(BaseModel):
"""Filters for listing issues"""
diff --git a/plugins/gitea/schemas/pull_request.py b/plugins/gitea/schemas/pull_request.py
index 85a01c8..0d91d01 100644
--- a/plugins/gitea/schemas/pull_request.py
+++ b/plugins/gitea/schemas/pull_request.py
@@ -11,6 +11,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
from .common import GiteaUser
from .issue import Label, Milestone
+
class PRBranchInfo(BaseModel):
"""Pull request branch information"""
@@ -22,6 +23,7 @@ class PRBranchInfo(BaseModel):
repo_id: int
repository: dict | None = None
+
class PullRequest(BaseModel):
"""Gitea pull request model"""
@@ -58,6 +60,7 @@ class PullRequest(BaseModel):
allow_maintainer_edit: bool = False
url: str
+
class PRReview(BaseModel):
"""Pull request review"""
@@ -72,6 +75,7 @@ class PRReview(BaseModel):
pull_request_url: str
submitted_at: datetime
+
class PRCommit(BaseModel):
"""Pull request commit"""
@@ -86,6 +90,7 @@ class PRCommit(BaseModel):
committer: GiteaUser | None = None
parents: list[dict] = []
+
class PRFile(BaseModel):
"""Pull request file change"""
@@ -101,6 +106,7 @@ class PRFile(BaseModel):
patch: str | None = None
previous_filename: str | None = None
+
class CreatePullRequestRequest(BaseModel):
"""Request to create a pull request"""
@@ -116,6 +122,7 @@ class CreatePullRequestRequest(BaseModel):
milestone: int | None = Field(None, description="Milestone ID")
due_date: datetime | None = Field(None, description="Due date")
+
class UpdatePullRequestRequest(BaseModel):
"""Request to update a pull request"""
@@ -139,6 +146,7 @@ class UpdatePullRequestRequest(BaseModel):
raise ValueError("State must be 'open' or 'closed'")
return v
+
class MergePullRequestRequest(BaseModel):
"""Request to merge a pull request"""
@@ -162,6 +170,7 @@ class MergePullRequestRequest(BaseModel):
raise ValueError(f"Merge method must be one of: {', '.join(allowed)}")
return v
+
class CreateReviewRequest(BaseModel):
"""Request to create a review"""
@@ -179,6 +188,7 @@ class CreateReviewRequest(BaseModel):
raise ValueError(f"Review event must be one of: {', '.join(allowed)}")
return v
+
class RequestReviewersRequest(BaseModel):
"""Request reviewers for a pull request"""
@@ -187,6 +197,7 @@ class RequestReviewersRequest(BaseModel):
reviewers: list[str] = Field(..., min_items=1, description="List of reviewer usernames")
team_reviewers: list[str] | None = Field(None, description="List of team slugs")
+
class PRListFilters(BaseModel):
"""Filters for listing pull requests"""
diff --git a/plugins/gitea/schemas/repository.py b/plugins/gitea/schemas/repository.py
index b314f5f..91a66a5 100644
--- a/plugins/gitea/schemas/repository.py
+++ b/plugins/gitea/schemas/repository.py
@@ -11,6 +11,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
from .common import GiteaPermissions, GiteaUser
+
class Repository(BaseModel):
"""Gitea repository model"""
@@ -59,6 +60,7 @@ class Repository(BaseModel):
default_merge_style: str = "merge"
avatar_url: str | None = None
+
class Branch(BaseModel):
"""Git branch model"""
@@ -73,6 +75,7 @@ class Branch(BaseModel):
user_can_push: bool = False
user_can_merge: bool = False
+
class Tag(BaseModel):
"""Git tag model"""
@@ -85,6 +88,7 @@ class Tag(BaseModel):
zipball_url: str | None = None
tarball_url: str | None = None
+
class FileContent(BaseModel):
"""File content model"""
@@ -104,6 +108,7 @@ class FileContent(BaseModel):
target: str | None = None # For symlinks
submodule_git_url: str | None = None
+
class CreateRepositoryRequest(BaseModel):
"""Request to create a repository"""
@@ -131,6 +136,7 @@ class CreateRepositoryRequest(BaseModel):
)
return v
+
class UpdateRepositoryRequest(BaseModel):
"""Request to update a repository"""
@@ -148,6 +154,7 @@ class UpdateRepositoryRequest(BaseModel):
allow_rebase: bool | None = Field(None, description="Allow rebase")
allow_squash_merge: bool | None = Field(None, description="Allow squash merge")
+
class CreateBranchRequest(BaseModel):
"""Request to create a branch"""
@@ -157,6 +164,7 @@ class CreateBranchRequest(BaseModel):
old_branch_name: str | None = Field(None, description="Source branch (default: default branch)")
old_ref_name: str | None = Field(None, description="Source commit SHA or ref")
+
class CreateTagRequest(BaseModel):
"""Request to create a tag"""
@@ -166,6 +174,7 @@ class CreateTagRequest(BaseModel):
message: str | None = Field(None, description="Tag message")
target: str | None = Field(None, description="Target commit SHA (default: latest)")
+
class CreateFileRequest(BaseModel):
"""Request to create a file"""
@@ -180,6 +189,7 @@ class CreateFileRequest(BaseModel):
committer_email: str | None = Field(None, description="Committer email")
new_branch: str | None = Field(None, description="Create new branch for this commit")
+
class UpdateFileRequest(BaseModel):
"""Request to update a file"""
diff --git a/plugins/gitea/schemas/user.py b/plugins/gitea/schemas/user.py
index 15ad8d7..2cf6f7c 100644
--- a/plugins/gitea/schemas/user.py
+++ b/plugins/gitea/schemas/user.py
@@ -8,6 +8,7 @@ from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
+
class User(BaseModel):
"""Gitea user model"""
@@ -34,6 +35,7 @@ class User(BaseModel):
starred_repos_count: int = 0
username: str | None = None # Alias for login
+
class Organization(BaseModel):
"""Gitea organization model"""
@@ -50,6 +52,7 @@ class Organization(BaseModel):
repo_admin_change_team_access: bool = False
username: str | None = None # Alias for name
+
class Team(BaseModel):
"""Organization team model"""
@@ -65,6 +68,7 @@ class Team(BaseModel):
units: list[str] | None = None
units_map: dict | None = None
+
class TeamMember(BaseModel):
"""Team member model"""
@@ -73,6 +77,7 @@ class TeamMember(BaseModel):
user: User
role: str | None = None # Role in team
+
class Email(BaseModel):
"""User email model"""
@@ -82,6 +87,7 @@ class Email(BaseModel):
verified: bool
primary: bool
+
class SearchUsersRequest(BaseModel):
"""Request to search users"""
@@ -90,6 +96,7 @@ class SearchUsersRequest(BaseModel):
q: str | None = Field(None, description="Search query")
uid: int | None = Field(None, description="User ID")
+
class SearchOrgsRequest(BaseModel):
"""Request to search organizations"""
diff --git a/plugins/gitea/schemas/webhook.py b/plugins/gitea/schemas/webhook.py
index 3397bec..df107f6 100644
--- a/plugins/gitea/schemas/webhook.py
+++ b/plugins/gitea/schemas/webhook.py
@@ -9,6 +9,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
+
class Webhook(BaseModel):
"""Gitea webhook model"""
@@ -22,6 +23,7 @@ class Webhook(BaseModel):
updated_at: datetime
created_at: datetime
+
class WebhookConfig(BaseModel):
"""Webhook configuration"""
@@ -33,6 +35,7 @@ class WebhookConfig(BaseModel):
http_method: str | None = "POST"
branch_filter: str | None = None
+
class CreateWebhookRequest(BaseModel):
"""Request to create a webhook"""
@@ -109,6 +112,7 @@ class CreateWebhookRequest(BaseModel):
v["content_type"] = "json"
return v
+
class UpdateWebhookRequest(BaseModel):
"""Request to update a webhook"""
@@ -152,6 +156,7 @@ class UpdateWebhookRequest(BaseModel):
raise ValueError(f"Invalid event: {event}")
return v
+
class WebhookTestResult(BaseModel):
"""Webhook test result"""
diff --git a/plugins/n8n/client.py b/plugins/n8n/client.py
index f44fe5d..990d523 100644
--- a/plugins/n8n/client.py
+++ b/plugins/n8n/client.py
@@ -10,6 +10,7 @@ from typing import Any
import aiohttp
+
class N8nClient:
"""
n8n REST API client for HTTP communication.
diff --git a/plugins/n8n/handlers/credentials.py b/plugins/n8n/handlers/credentials.py
index 12c70c9..8c17571 100644
--- a/plugins/n8n/handlers/credentials.py
+++ b/plugins/n8n/handlers/credentials.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.n8n.client import N8nClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -85,9 +86,11 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# === HANDLER FUNCTIONS ===
# Note: list_credentials is not supported in n8n Public API (GET /credentials returns 405)
+
async def get_credential(client: N8nClient, credential_id: str) -> str:
"""Get credential metadata"""
try:
@@ -106,6 +109,7 @@ async def get_credential(client: N8nClient, credential_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_credential(client: N8nClient, name: str, type: str, data: dict[str, Any]) -> str:
"""Create a new credential"""
try:
@@ -124,6 +128,7 @@ async def create_credential(client: N8nClient, name: str, type: str, data: dict[
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_credential(client: N8nClient, credential_id: str) -> str:
"""Delete a credential"""
try:
@@ -134,6 +139,7 @@ async def delete_credential(client: N8nClient, credential_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_credential_schema(client: N8nClient, credential_type: str) -> str:
"""Get credential type schema"""
try:
@@ -144,6 +150,7 @@ async def get_credential_schema(client: N8nClient, credential_type: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def transfer_credential(
client: N8nClient, credential_id: str, destination_project_id: str
) -> str:
diff --git a/plugins/n8n/handlers/executions.py b/plugins/n8n/handlers/executions.py
index cdc3c6f..302fb0b 100644
--- a/plugins/n8n/handlers/executions.py
+++ b/plugins/n8n/handlers/executions.py
@@ -6,6 +6,7 @@ from typing import Any
from plugins.n8n.client import N8nClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -191,8 +192,10 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# === HANDLER FUNCTIONS ===
+
async def list_executions(
client: N8nClient,
workflow_id: str | None = None,
@@ -238,6 +241,7 @@ async def list_executions(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_execution(client: N8nClient, execution_id: str, include_data: bool = True) -> str:
"""Get execution details"""
try:
@@ -268,6 +272,7 @@ async def get_execution(client: N8nClient, execution_id: str, include_data: bool
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_execution(client: N8nClient, execution_id: str) -> str:
"""Delete a single execution"""
try:
@@ -280,6 +285,7 @@ async def delete_execution(client: N8nClient, execution_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_executions(client: N8nClient, execution_ids: list[str]) -> str:
"""Bulk delete executions"""
try:
@@ -305,6 +311,7 @@ async def delete_executions(client: N8nClient, execution_ids: list[str]) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def stop_execution(client: N8nClient, execution_id: str) -> str:
"""Stop a running execution"""
try:
@@ -317,6 +324,7 @@ async def stop_execution(client: N8nClient, execution_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def retry_execution(client: N8nClient, execution_id: str) -> str:
"""Retry a failed execution"""
try:
@@ -360,6 +368,7 @@ async def retry_execution(client: N8nClient, execution_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_execution_data(client: N8nClient, execution_id: str) -> str:
"""Get full execution data"""
try:
@@ -383,6 +392,7 @@ async def get_execution_data(client: N8nClient, execution_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def wait_for_execution(
client: N8nClient, execution_id: str, timeout_seconds: int = 60, poll_interval: int = 2
) -> str:
diff --git a/plugins/n8n/handlers/projects.py b/plugins/n8n/handlers/projects.py
index 8f14525..5008ec0 100644
--- a/plugins/n8n/handlers/projects.py
+++ b/plugins/n8n/handlers/projects.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.n8n.client import N8nClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -132,6 +133,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def list_projects(client: N8nClient, limit: int = 100, cursor: str | None = None) -> str:
try:
response = await client.list_projects(limit=limit, cursor=cursor)
@@ -146,6 +148,7 @@ async def list_projects(client: N8nClient, limit: int = 100, cursor: str | None
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_project(client: N8nClient, project_id: str) -> str:
try:
project = await client.get_project(project_id)
@@ -156,6 +159,7 @@ async def get_project(client: N8nClient, project_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_project(client: N8nClient, name: str) -> str:
try:
project = await client.create_project(name)
@@ -170,6 +174,7 @@ async def create_project(client: N8nClient, name: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_project(client: N8nClient, project_id: str, name: str) -> str:
try:
project = await client.update_project(project_id, name)
@@ -184,6 +189,7 @@ async def update_project(client: N8nClient, project_id: str, name: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_project(client: N8nClient, project_id: str) -> str:
try:
await client.delete_project(project_id)
@@ -191,6 +197,7 @@ async def delete_project(client: N8nClient, project_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def add_project_users(client: N8nClient, project_id: str, users: list[dict[str, str]]) -> str:
try:
relations = [{"userId": u["user_id"], "role": u["role"]} for u in users]
@@ -202,6 +209,7 @@ async def add_project_users(client: N8nClient, project_id: str, users: list[dict
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def change_project_user_role(
client: N8nClient, project_id: str, user_id: str, role: str
) -> str:
@@ -217,6 +225,7 @@ async def change_project_user_role(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def remove_project_user(client: N8nClient, project_id: str, user_id: str) -> str:
try:
await client.remove_project_user(project_id, user_id)
diff --git a/plugins/n8n/handlers/system.py b/plugins/n8n/handlers/system.py
index 8652f4a..393db12 100644
--- a/plugins/n8n/handlers/system.py
+++ b/plugins/n8n/handlers/system.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.n8n.client import N8nClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -60,6 +61,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def run_security_audit(client: N8nClient, categories: list[str] | None = None) -> str:
"""Run security audit"""
try:
@@ -84,6 +86,7 @@ async def run_security_audit(client: N8nClient, categories: list[str] | None = N
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def source_control_pull(
client: N8nClient, variables: dict[str, str] | None = None, force: bool = False
) -> str:
@@ -98,6 +101,7 @@ async def source_control_pull(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_instance_info(client: N8nClient) -> str:
"""Get instance information"""
try:
@@ -129,6 +133,7 @@ async def get_instance_info(client: N8nClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def health_check(client: N8nClient) -> str:
"""Check instance health"""
try:
diff --git a/plugins/n8n/handlers/tags.py b/plugins/n8n/handlers/tags.py
index 3bdf09f..b1ed212 100644
--- a/plugins/n8n/handlers/tags.py
+++ b/plugins/n8n/handlers/tags.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.n8n.client import N8nClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -85,6 +86,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def list_tags(client: N8nClient, limit: int = 100, cursor: str | None = None) -> str:
try:
response = await client.list_tags(limit=limit, cursor=cursor)
@@ -99,6 +101,7 @@ async def list_tags(client: N8nClient, limit: int = 100, cursor: str | None = No
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_tag(client: N8nClient, tag_id: str) -> str:
try:
tag = await client.get_tag(tag_id)
@@ -108,6 +111,7 @@ async def get_tag(client: N8nClient, tag_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_tag(client: N8nClient, name: str) -> str:
try:
tag = await client.create_tag(name)
@@ -122,6 +126,7 @@ async def create_tag(client: N8nClient, name: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_tag(client: N8nClient, tag_id: str, name: str) -> str:
try:
tag = await client.update_tag(tag_id, name)
@@ -136,6 +141,7 @@ async def update_tag(client: N8nClient, tag_id: str, name: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_tag(client: N8nClient, tag_id: str) -> str:
try:
await client.delete_tag(tag_id)
@@ -143,6 +149,7 @@ async def delete_tag(client: N8nClient, tag_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_tags(client: N8nClient, tag_ids: list[str]) -> str:
try:
deleted, failed = [], []
diff --git a/plugins/n8n/handlers/users.py b/plugins/n8n/handlers/users.py
index 984a313..4bac1e2 100644
--- a/plugins/n8n/handlers/users.py
+++ b/plugins/n8n/handlers/users.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.n8n.client import N8nClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -89,6 +90,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def list_users(
client: N8nClient, limit: int = 100, cursor: str | None = None, include_role: bool = True
) -> str:
@@ -115,6 +117,7 @@ async def list_users(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_user(client: N8nClient, user_id: str, include_role: bool = True) -> str:
try:
user = await client.get_user(user_id, include_role)
@@ -135,6 +138,7 @@ async def get_user(client: N8nClient, user_id: str, include_role: bool = True) -
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_user(client: N8nClient, email: str, role: str = "global:member") -> str:
try:
users = [{"email": email, "role": role}]
@@ -150,6 +154,7 @@ async def create_user(client: N8nClient, email: str, role: str = "global:member"
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_user(client: N8nClient, user_id: str) -> str:
try:
await client.delete_user(user_id)
@@ -157,6 +162,7 @@ async def delete_user(client: N8nClient, user_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def change_user_role(client: N8nClient, user_id: str, new_role: str) -> str:
try:
await client.change_user_role(user_id, new_role)
diff --git a/plugins/n8n/handlers/variables.py b/plugins/n8n/handlers/variables.py
index b8b71aa..8130820 100644
--- a/plugins/n8n/handlers/variables.py
+++ b/plugins/n8n/handlers/variables.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.n8n.client import N8nClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -92,6 +93,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def list_variables(client: N8nClient, limit: int = 100, cursor: str | None = None) -> str:
try:
response = await client.list_variables(limit=limit, cursor=cursor)
@@ -106,6 +108,7 @@ async def list_variables(client: N8nClient, limit: int = 100, cursor: str | None
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_variable(client: N8nClient, key: str) -> str:
try:
variable = await client.get_variable(key)
@@ -119,6 +122,7 @@ async def get_variable(client: N8nClient, key: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_variable(client: N8nClient, key: str, value: str) -> str:
try:
await client.create_variable(key, value)
@@ -133,6 +137,7 @@ async def create_variable(client: N8nClient, key: str, value: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_variable(client: N8nClient, key: str, value: str) -> str:
try:
await client.update_variable(key, value)
@@ -147,6 +152,7 @@ async def update_variable(client: N8nClient, key: str, value: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_variable(client: N8nClient, key: str) -> str:
try:
await client.delete_variable(key)
@@ -154,6 +160,7 @@ async def delete_variable(client: N8nClient, key: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def set_variables(client: N8nClient, variables: dict[str, str]) -> str:
try:
created, updated, failed = [], [], []
diff --git a/plugins/n8n/handlers/workflows.py b/plugins/n8n/handlers/workflows.py
index 72d8b6b..06b41db 100644
--- a/plugins/n8n/handlers/workflows.py
+++ b/plugins/n8n/handlers/workflows.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.n8n.client import N8nClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -319,8 +320,10 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# === HANDLER FUNCTIONS ===
+
async def list_workflows(
client: N8nClient,
active: bool | None = None,
@@ -360,6 +363,7 @@ async def list_workflows(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_workflow(client: N8nClient, workflow_id: str) -> str:
"""Get workflow details"""
try:
@@ -386,6 +390,7 @@ async def get_workflow(client: N8nClient, workflow_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def create_workflow(
client: N8nClient,
name: str,
@@ -426,6 +431,7 @@ async def create_workflow(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def update_workflow(
client: N8nClient,
workflow_id: str,
@@ -466,6 +472,7 @@ async def update_workflow(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def delete_workflow(client: N8nClient, workflow_id: str) -> str:
"""Delete a workflow"""
try:
@@ -478,6 +485,7 @@ async def delete_workflow(client: N8nClient, workflow_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def activate_workflow(client: N8nClient, workflow_id: str) -> str:
"""Activate a workflow"""
try:
@@ -498,6 +506,7 @@ async def activate_workflow(client: N8nClient, workflow_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def deactivate_workflow(client: N8nClient, workflow_id: str) -> str:
"""Deactivate a workflow"""
try:
@@ -518,6 +527,7 @@ async def deactivate_workflow(client: N8nClient, workflow_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def execute_workflow(client: N8nClient, workflow_id: str) -> str:
"""Execute a workflow manually"""
try:
@@ -538,6 +548,7 @@ async def execute_workflow(client: N8nClient, workflow_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def execute_workflow_with_data(
client: N8nClient, workflow_id: str, data: dict[str, Any]
) -> str:
@@ -561,6 +572,7 @@ async def execute_workflow_with_data(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def duplicate_workflow(client: N8nClient, workflow_id: str, new_name: str) -> str:
"""Duplicate a workflow"""
try:
@@ -589,6 +601,7 @@ async def duplicate_workflow(client: N8nClient, workflow_id: str, new_name: str)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def export_workflow(client: N8nClient, workflow_id: str) -> str:
"""Export workflow as JSON"""
try:
@@ -602,6 +615,7 @@ async def export_workflow(client: N8nClient, workflow_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def import_workflow(
client: N8nClient, workflow_json: dict[str, Any], name_override: str | None = None
) -> str:
@@ -634,6 +648,7 @@ async def import_workflow(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def get_workflow_tags(client: N8nClient, workflow_id: str) -> str:
"""Get tags assigned to a workflow"""
try:
@@ -646,6 +661,7 @@ async def get_workflow_tags(client: N8nClient, workflow_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
+
async def set_workflow_tags(
client: N8nClient, workflow_id: str, tag_ids: list[str] | None = None
) -> str:
diff --git a/plugins/n8n/plugin.py b/plugins/n8n/plugin.py
index e0bfe4b..dbb2225 100644
--- a/plugins/n8n/plugin.py
+++ b/plugins/n8n/plugin.py
@@ -12,6 +12,7 @@ from plugins.base import BasePlugin
from plugins.n8n import handlers
from plugins.n8n.client import N8nClient
+
class N8nPlugin(BasePlugin):
"""
n8n Automation Plugin - Comprehensive workflow management.
diff --git a/plugins/openpanel/client.py b/plugins/openpanel/client.py
index 38a8d0b..c911e42 100644
--- a/plugins/openpanel/client.py
+++ b/plugins/openpanel/client.py
@@ -29,6 +29,7 @@ from typing import Any
import aiohttp
+
class OpenPanelClient:
"""
OpenPanel Self-Hosted API client.
diff --git a/plugins/openpanel/handlers/clients.py b/plugins/openpanel/handlers/clients.py
index b6c6b4a..e5fde47 100644
--- a/plugins/openpanel/handlers/clients.py
+++ b/plugins/openpanel/handlers/clients.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.openpanel.client import OpenPanelClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (6 tools)"""
return [
@@ -112,10 +113,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Client Functions (6)
# =====================
+
async def list_clients(client: OpenPanelClient, project_id: str) -> str:
"""List all API clients"""
try:
@@ -132,6 +135,7 @@ async def list_clients(client: OpenPanelClient, project_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_client(client: OpenPanelClient, project_id: str, client_id: str) -> str:
"""Get API client details"""
try:
@@ -149,6 +153,7 @@ async def get_client(client: OpenPanelClient, project_id: str, client_id: str) -
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def create_client(
client: OpenPanelClient,
project_id: str,
@@ -181,6 +186,7 @@ async def create_client(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_client(client: OpenPanelClient, project_id: str, client_id: str) -> str:
"""Delete an API client"""
try:
@@ -199,6 +205,7 @@ async def delete_client(client: OpenPanelClient, project_id: str, client_id: str
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def regenerate_client_secret(client: OpenPanelClient, project_id: str, client_id: str) -> str:
"""Regenerate client secret"""
try:
@@ -217,6 +224,7 @@ async def regenerate_client_secret(client: OpenPanelClient, project_id: str, cli
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def update_client_mode(
client: OpenPanelClient,
project_id: str,
diff --git a/plugins/openpanel/handlers/dashboards.py b/plugins/openpanel/handlers/dashboards.py
index 581e846..930238e 100644
--- a/plugins/openpanel/handlers/dashboards.py
+++ b/plugins/openpanel/handlers/dashboards.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.openpanel.client import OpenPanelClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (10 tools)"""
return [
@@ -215,10 +216,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Dashboard Functions (10)
# =====================
+
async def list_dashboards(client: OpenPanelClient, project_id: str) -> str:
"""List all dashboards"""
try:
@@ -235,6 +238,7 @@ async def list_dashboards(client: OpenPanelClient, project_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_dashboard(client: OpenPanelClient, project_id: str, dashboard_id: str) -> str:
"""Get dashboard details"""
try:
@@ -252,6 +256,7 @@ async def get_dashboard(client: OpenPanelClient, project_id: str, dashboard_id:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def create_dashboard(
client: OpenPanelClient, project_id: str, name: str, description: str | None = None
) -> str:
@@ -273,6 +278,7 @@ async def create_dashboard(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def update_dashboard(
client: OpenPanelClient,
project_id: str,
@@ -303,6 +309,7 @@ async def update_dashboard(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_dashboard(client: OpenPanelClient, project_id: str, dashboard_id: str) -> str:
"""Delete a dashboard"""
try:
@@ -320,6 +327,7 @@ async def delete_dashboard(client: OpenPanelClient, project_id: str, dashboard_i
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def add_chart(
client: OpenPanelClient,
project_id: str,
@@ -353,6 +361,7 @@ async def add_chart(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def update_chart(
client: OpenPanelClient,
project_id: str,
@@ -388,6 +397,7 @@ async def update_chart(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_chart(
client: OpenPanelClient, project_id: str, dashboard_id: str, chart_id: str
) -> str:
@@ -408,6 +418,7 @@ async def delete_chart(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def duplicate_dashboard(
client: OpenPanelClient, project_id: str, dashboard_id: str, new_name: str
) -> str:
@@ -428,6 +439,7 @@ async def duplicate_dashboard(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def share_dashboard(
client: OpenPanelClient,
project_id: str,
diff --git a/plugins/openpanel/handlers/events.py b/plugins/openpanel/handlers/events.py
index 143f3ac..b49e0b4 100644
--- a/plugins/openpanel/handlers/events.py
+++ b/plugins/openpanel/handlers/events.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.openpanel.client import OpenPanelClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (10 tools)"""
return [
@@ -302,10 +303,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Event Tracking Functions (10)
# =====================
+
async def track_event(
client: OpenPanelClient,
name: str,
@@ -340,6 +343,7 @@ async def track_event(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def track_page_view(
client: OpenPanelClient,
path: str,
@@ -384,6 +388,7 @@ async def track_page_view(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def track_screen_view(
client: OpenPanelClient,
screen_name: str,
@@ -425,6 +430,7 @@ async def track_screen_view(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def identify_user(
client: OpenPanelClient,
profile_id: str,
@@ -461,6 +467,7 @@ async def identify_user(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def set_user_properties(
client: OpenPanelClient, profile_id: str, properties: dict[str, Any]
) -> str:
@@ -482,6 +489,7 @@ async def set_user_properties(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def increment_property(
client: OpenPanelClient, profile_id: str, property_name: str, value: int = 1
) -> str:
@@ -519,6 +527,7 @@ async def increment_property(
ensure_ascii=False,
)
+
async def decrement_property(
client: OpenPanelClient, profile_id: str, property_name: str, value: int = 1
) -> str:
@@ -556,6 +565,7 @@ async def decrement_property(
ensure_ascii=False,
)
+
async def alias_user(client: OpenPanelClient, profile_id: str, alias: str) -> str:
"""Create an alias to link two profile IDs"""
try:
@@ -583,6 +593,7 @@ async def alias_user(client: OpenPanelClient, profile_id: str, alias: str) -> st
ensure_ascii=False,
)
+
async def track_revenue(
client: OpenPanelClient,
amount: float,
@@ -629,6 +640,7 @@ async def track_revenue(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def track_batch(
client: OpenPanelClient,
events: list[dict[str, Any]],
diff --git a/plugins/openpanel/handlers/export.py b/plugins/openpanel/handlers/export.py
index d6c3b71..4d6612a 100644
--- a/plugins/openpanel/handlers/export.py
+++ b/plugins/openpanel/handlers/export.py
@@ -10,6 +10,7 @@ from typing import Any
from plugins.openpanel.client import OpenPanelClient
from plugins.openpanel.handlers.utils import get_project_id as _get_project_id
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (10 tools)"""
return [
@@ -375,10 +376,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Export Functions (10)
# =====================
+
async def export_events(
client: OpenPanelClient,
project_id: str | None = None,
@@ -422,6 +425,7 @@ async def export_events(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def export_events_csv(
client: OpenPanelClient,
project_id: str | None = None,
@@ -480,6 +484,7 @@ async def export_events_csv(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def export_chart_data(
client: OpenPanelClient,
events: list[dict[str, Any]],
@@ -517,6 +522,7 @@ async def export_chart_data(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_event_count(
client: OpenPanelClient,
project_id: str | None = None,
@@ -555,6 +561,7 @@ async def get_event_count(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_unique_users(
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d"
) -> str:
@@ -605,6 +612,7 @@ async def get_unique_users(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_page_views(
client: OpenPanelClient,
project_id: str | None = None,
@@ -660,6 +668,7 @@ async def get_page_views(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_top_pages(
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d", limit: int = 10
) -> str:
@@ -707,6 +716,7 @@ async def get_top_pages(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_top_referrers(
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d", limit: int = 10
) -> str:
@@ -764,6 +774,7 @@ async def get_top_referrers(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_geo_data(
client: OpenPanelClient,
project_id: str | None = None,
@@ -827,6 +838,7 @@ async def get_geo_data(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_device_data(
client: OpenPanelClient,
project_id: str | None = None,
diff --git a/plugins/openpanel/handlers/funnels.py b/plugins/openpanel/handlers/funnels.py
index 5ba5f45..09b9233 100644
--- a/plugins/openpanel/handlers/funnels.py
+++ b/plugins/openpanel/handlers/funnels.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.openpanel.client import OpenPanelClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (8 tools)"""
return [
@@ -189,10 +190,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Funnel Functions (8)
# =====================
+
async def list_funnels(client: OpenPanelClient, project_id: str) -> str:
"""List all funnels for a project"""
try:
@@ -209,6 +212,7 @@ async def list_funnels(client: OpenPanelClient, project_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_funnel(
client: OpenPanelClient, project_id: str, funnel_id: str, date_range: str = "30d"
) -> str:
@@ -229,6 +233,7 @@ async def get_funnel(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def create_funnel(
client: OpenPanelClient,
project_id: str,
@@ -262,6 +267,7 @@ async def create_funnel(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def update_funnel(
client: OpenPanelClient,
project_id: str,
@@ -295,6 +301,7 @@ async def update_funnel(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_funnel(client: OpenPanelClient, project_id: str, funnel_id: str) -> str:
"""Delete a funnel"""
try:
@@ -312,6 +319,7 @@ async def delete_funnel(client: OpenPanelClient, project_id: str, funnel_id: str
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_funnel_conversion(
client: OpenPanelClient, project_id: str, funnel_id: str, date_range: str = "30d"
) -> str:
@@ -332,6 +340,7 @@ async def get_funnel_conversion(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_funnel_breakdown(
client: OpenPanelClient,
project_id: str,
@@ -357,6 +366,7 @@ async def get_funnel_breakdown(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def compare_funnels(
client: OpenPanelClient, project_id: str, funnel_ids: list[str], date_range: str = "30d"
) -> str:
diff --git a/plugins/openpanel/handlers/profiles.py b/plugins/openpanel/handlers/profiles.py
index 62ce2b0..399dcb1 100644
--- a/plugins/openpanel/handlers/profiles.py
+++ b/plugins/openpanel/handlers/profiles.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.openpanel.client import OpenPanelClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (8 tools)"""
return [
@@ -183,10 +184,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Profile Functions (8)
# =====================
+
async def list_profiles(
client: OpenPanelClient,
project_id: str,
@@ -210,6 +213,7 @@ async def list_profiles(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_profile(client: OpenPanelClient, project_id: str, profile_id: str) -> str:
"""Get profile details"""
try:
@@ -249,6 +253,7 @@ async def get_profile(client: OpenPanelClient, project_id: str, profile_id: str)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def search_profiles(
client: OpenPanelClient,
project_id: str,
@@ -278,6 +283,7 @@ async def search_profiles(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_profile_events(
client: OpenPanelClient,
project_id: str,
@@ -308,6 +314,7 @@ async def get_profile_events(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_profile_sessions(
client: OpenPanelClient, project_id: str, profile_id: str, limit: int = 20
) -> str:
@@ -334,6 +341,7 @@ async def get_profile_sessions(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_profile(
client: OpenPanelClient, project_id: str, profile_id: str, confirm: bool = False
) -> str:
@@ -365,6 +373,7 @@ async def delete_profile(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def merge_profiles(
client: OpenPanelClient, project_id: str, primary_profile_id: str, secondary_profile_id: str
) -> str:
@@ -385,6 +394,7 @@ async def merge_profiles(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def export_profile_data(
client: OpenPanelClient, project_id: str, profile_id: str, format: str = "json"
) -> str:
diff --git a/plugins/openpanel/handlers/projects.py b/plugins/openpanel/handlers/projects.py
index 4e93570..177f688 100644
--- a/plugins/openpanel/handlers/projects.py
+++ b/plugins/openpanel/handlers/projects.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.openpanel.client import OpenPanelClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (8 tools)"""
return [
@@ -140,10 +141,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Project Functions (8)
# =====================
+
async def list_projects(client: OpenPanelClient) -> str:
"""List all projects"""
try:
@@ -159,6 +162,7 @@ async def list_projects(client: OpenPanelClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_project(client: OpenPanelClient, project_id: str) -> str:
"""Get project details"""
try:
@@ -175,6 +179,7 @@ async def get_project(client: OpenPanelClient, project_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def create_project(
client: OpenPanelClient, name: str, domain: str | None = None, timezone: str = "UTC"
) -> str:
@@ -195,6 +200,7 @@ async def create_project(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def update_project(
client: OpenPanelClient,
project_id: str,
@@ -226,6 +232,7 @@ async def update_project(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_project(client: OpenPanelClient, project_id: str, confirm: bool = False) -> str:
"""Delete a project"""
try:
@@ -254,6 +261,7 @@ async def delete_project(client: OpenPanelClient, project_id: str, confirm: bool
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_project_stats(
client: OpenPanelClient, project_id: str, date_range: str = "30d"
) -> str:
@@ -298,6 +306,7 @@ async def get_project_stats(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_project_settings(client: OpenPanelClient, project_id: str) -> str:
"""Get project settings"""
try:
@@ -314,6 +323,7 @@ async def get_project_settings(client: OpenPanelClient, project_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def update_project_settings(
client: OpenPanelClient, project_id: str, settings: dict[str, Any]
) -> str:
diff --git a/plugins/openpanel/handlers/reports.py b/plugins/openpanel/handlers/reports.py
index 8bd6662..be1332e 100644
--- a/plugins/openpanel/handlers/reports.py
+++ b/plugins/openpanel/handlers/reports.py
@@ -9,6 +9,7 @@ from typing import Any
from plugins.openpanel.client import OpenPanelClient
from plugins.openpanel.handlers.utils import get_project_id as _get_project_id
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (8 tools)"""
return [
@@ -244,10 +245,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Report Functions (8)
# =====================
+
async def get_overview_report(
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d"
) -> str:
@@ -310,6 +313,7 @@ async def get_overview_report(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_retention_report(
client: OpenPanelClient,
project_id: str | None = None,
@@ -343,6 +347,7 @@ async def get_retention_report(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_cohort_report(
client: OpenPanelClient,
project_id: str | None = None,
@@ -373,6 +378,7 @@ async def get_cohort_report(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_paths_report(
client: OpenPanelClient,
project_id: str | None = None,
@@ -403,6 +409,7 @@ async def get_paths_report(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_realtime_stats(client: OpenPanelClient, project_id: str | None = None) -> str:
"""Get real-time visitor statistics using chart.chart with 30min range"""
try:
@@ -443,6 +450,7 @@ async def get_realtime_stats(client: OpenPanelClient, project_id: str | None = N
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_ab_test_results(
client: OpenPanelClient,
test_name: str,
@@ -469,6 +477,7 @@ async def get_ab_test_results(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def create_scheduled_report(
client: OpenPanelClient,
name: str,
@@ -499,6 +508,7 @@ async def create_scheduled_report(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def export_report_pdf(
client: OpenPanelClient,
report_type: str,
diff --git a/plugins/openpanel/handlers/system.py b/plugins/openpanel/handlers/system.py
index 291fcc6..405f2bb 100644
--- a/plugins/openpanel/handlers/system.py
+++ b/plugins/openpanel/handlers/system.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.openpanel.client import OpenPanelClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (6 tools)"""
return [
@@ -72,10 +73,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# System Functions (6)
# =====================
+
async def health_check(client: OpenPanelClient) -> str:
"""Check OpenPanel instance health"""
try:
@@ -109,6 +112,7 @@ async def health_check(client: OpenPanelClient) -> str:
ensure_ascii=False,
)
+
async def get_instance_info(client: OpenPanelClient) -> str:
"""Get OpenPanel instance information"""
try:
@@ -126,6 +130,7 @@ async def get_instance_info(client: OpenPanelClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_usage_stats(client: OpenPanelClient, project_id: str, date_range: str = "30d") -> str:
"""Get usage statistics for a project"""
try:
@@ -173,6 +178,7 @@ async def get_usage_stats(client: OpenPanelClient, project_id: str, date_range:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_storage_stats(client: OpenPanelClient, project_id: str) -> str:
"""Get storage usage statistics"""
try:
@@ -199,6 +205,7 @@ async def get_storage_stats(client: OpenPanelClient, project_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def test_connection(client: OpenPanelClient) -> str:
"""Test connection to OpenPanel API"""
try:
@@ -232,6 +239,7 @@ async def test_connection(client: OpenPanelClient) -> str:
ensure_ascii=False,
)
+
async def get_rate_limit_status(client: OpenPanelClient) -> str:
"""Check current rate limit status"""
try:
diff --git a/plugins/openpanel/handlers/utils.py b/plugins/openpanel/handlers/utils.py
index c17c363..57b8722 100644
--- a/plugins/openpanel/handlers/utils.py
+++ b/plugins/openpanel/handlers/utils.py
@@ -2,6 +2,7 @@
from plugins.openpanel.client import OpenPanelClient
+
def get_project_id(client: OpenPanelClient, project_id: str | None) -> str:
"""
Get effective project_id, using default if not provided.
diff --git a/plugins/openpanel/plugin.py b/plugins/openpanel/plugin.py
index f23aea4..e0c11ef 100644
--- a/plugins/openpanel/plugin.py
+++ b/plugins/openpanel/plugin.py
@@ -13,6 +13,7 @@ from plugins.base import BasePlugin
from plugins.openpanel import handlers
from plugins.openpanel.client import OpenPanelClient
+
class OpenPanelPlugin(BasePlugin):
"""
OpenPanel Analytics Plugin - Comprehensive product analytics.
diff --git a/plugins/supabase/client.py b/plugins/supabase/client.py
index aa13c37..450c9cb 100644
--- a/plugins/supabase/client.py
+++ b/plugins/supabase/client.py
@@ -18,6 +18,7 @@ from typing import Any
import aiohttp
+
class SupabaseClient:
"""
Supabase Self-Hosted API client.
diff --git a/plugins/supabase/handlers/admin.py b/plugins/supabase/handlers/admin.py
index aeb31ae..39a42c7 100644
--- a/plugins/supabase/handlers/admin.py
+++ b/plugins/supabase/handlers/admin.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.supabase.client import SupabaseClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (12 tools)"""
return [
@@ -260,10 +261,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Admin Operations (12 tools)
# =====================
+
async def enable_extension(client: SupabaseClient, name: str, schema: str = "extensions") -> str:
"""Enable a PostgreSQL extension"""
try:
@@ -282,6 +285,7 @@ async def enable_extension(client: SupabaseClient, name: str, schema: str = "ext
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def disable_extension(client: SupabaseClient, name: str, cascade: bool = False) -> str:
"""Disable (drop) a PostgreSQL extension"""
try:
@@ -302,6 +306,7 @@ async def disable_extension(client: SupabaseClient, name: str, cascade: bool = F
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def create_policy(
client: SupabaseClient,
table: str,
@@ -350,6 +355,7 @@ async def create_policy(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def update_policy(
client: SupabaseClient,
table: str,
@@ -380,6 +386,7 @@ async def update_policy(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_policy(
client: SupabaseClient, table: str, name: str, schema: str = "public"
) -> str:
@@ -400,6 +407,7 @@ async def delete_policy(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def enable_rls(client: SupabaseClient, table: str, schema: str = "public") -> str:
"""Enable RLS on a table"""
try:
@@ -418,6 +426,7 @@ async def enable_rls(client: SupabaseClient, table: str, schema: str = "public")
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def disable_rls(client: SupabaseClient, table: str, schema: str = "public") -> str:
"""Disable RLS on a table"""
try:
@@ -437,6 +446,7 @@ async def disable_rls(client: SupabaseClient, table: str, schema: str = "public"
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def create_table(
client: SupabaseClient,
name: str,
@@ -491,6 +501,7 @@ async def create_table(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def drop_table(
client: SupabaseClient, name: str, schema: str = "public", cascade: bool = False
) -> str:
@@ -513,6 +524,7 @@ async def drop_table(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def add_column(
client: SupabaseClient,
table: str,
@@ -552,6 +564,7 @@ async def add_column(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def drop_column(
client: SupabaseClient,
table: str,
@@ -582,6 +595,7 @@ async def drop_column(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_database_size(client: SupabaseClient) -> str:
"""Get database and table sizes"""
try:
diff --git a/plugins/supabase/handlers/auth.py b/plugins/supabase/handlers/auth.py
index c11a3f1..4fac3b8 100644
--- a/plugins/supabase/handlers/auth.py
+++ b/plugins/supabase/handlers/auth.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.supabase.client import SupabaseClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (14 tools)"""
return [
@@ -281,10 +282,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Auth Operations (14 tools)
# =====================
+
async def list_users(client: SupabaseClient, page: int = 1, per_page: int = 50) -> str:
"""List all users with pagination"""
try:
@@ -307,6 +310,7 @@ async def list_users(client: SupabaseClient, page: int = 1, per_page: int = 50)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_user(client: SupabaseClient, user_id: str) -> str:
"""Get user by ID"""
try:
@@ -316,6 +320,7 @@ async def get_user(client: SupabaseClient, user_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def create_user(
client: SupabaseClient,
email: str,
@@ -344,6 +349,7 @@ async def create_user(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def update_user(
client: SupabaseClient,
user_id: str,
@@ -376,6 +382,7 @@ async def update_user(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_user(client: SupabaseClient, user_id: str) -> str:
"""Delete a user"""
try:
@@ -389,6 +396,7 @@ async def delete_user(client: SupabaseClient, user_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def invite_user(
client: SupabaseClient,
email: str,
@@ -409,6 +417,7 @@ async def invite_user(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def generate_link(
client: SupabaseClient, email: str, link_type: str = "magiclink", redirect_to: str | None = None
) -> str:
@@ -426,6 +435,7 @@ async def generate_link(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def ban_user(client: SupabaseClient, user_id: str, duration: str = "none") -> str:
"""Ban a user"""
try:
@@ -439,6 +449,7 @@ async def ban_user(client: SupabaseClient, user_id: str, duration: str = "none")
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def unban_user(client: SupabaseClient, user_id: str) -> str:
"""Unban a user"""
try:
@@ -452,6 +463,7 @@ async def unban_user(client: SupabaseClient, user_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def list_user_factors(client: SupabaseClient, user_id: str) -> str:
"""List user MFA factors"""
try:
@@ -463,6 +475,7 @@ async def list_user_factors(client: SupabaseClient, user_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_user_factor(client: SupabaseClient, user_id: str, factor_id: str) -> str:
"""Delete an MFA factor"""
try:
@@ -476,6 +489,7 @@ async def delete_user_factor(client: SupabaseClient, user_id: str, factor_id: st
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_auth_config(client: SupabaseClient) -> str:
"""Get auth configuration"""
try:
@@ -486,6 +500,7 @@ async def get_auth_config(client: SupabaseClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def search_users(
client: SupabaseClient, query: str, page: int = 1, per_page: int = 50
) -> str:
@@ -515,6 +530,7 @@ async def search_users(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_user_by_email(client: SupabaseClient, email: str) -> str:
"""Find user by email"""
try:
diff --git a/plugins/supabase/handlers/database.py b/plugins/supabase/handlers/database.py
index 9f722e5..c45f4ce 100644
--- a/plugins/supabase/handlers/database.py
+++ b/plugins/supabase/handlers/database.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.supabase.client import SupabaseClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (18 tools)"""
return [
@@ -439,10 +440,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# PostgREST Operations (6)
# =====================
+
async def query_table(
client: SupabaseClient,
table: str,
@@ -478,6 +481,7 @@ async def query_table(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def insert_rows(
client: SupabaseClient,
table: str,
@@ -509,6 +513,7 @@ async def insert_rows(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def update_rows(
client: SupabaseClient,
table: str,
@@ -545,6 +550,7 @@ async def update_rows(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_rows(
client: SupabaseClient, table: str, filters: list[dict], use_service_role: bool = False
) -> str:
@@ -577,6 +583,7 @@ async def delete_rows(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def execute_rpc(
client: SupabaseClient,
function_name: str,
@@ -597,6 +604,7 @@ async def execute_rpc(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def count_rows(
client: SupabaseClient,
table: str,
@@ -622,10 +630,12 @@ async def count_rows(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
# =====================
# Admin Operations via postgres-meta (12)
# =====================
+
async def list_tables(
client: SupabaseClient, schema: str = "public", limit: int = 50, offset: int = 0
) -> str:
@@ -660,6 +670,7 @@ async def list_tables(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_table_schema(client: SupabaseClient, table: str, schema: str = "public") -> str:
"""Get table schema/columns"""
try:
@@ -678,6 +689,7 @@ async def get_table_schema(client: SupabaseClient, table: str, schema: str = "pu
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def list_schemas(client: SupabaseClient) -> str:
"""List all database schemas"""
try:
@@ -695,6 +707,7 @@ async def list_schemas(client: SupabaseClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def list_extensions(client: SupabaseClient) -> str:
"""List installed extensions"""
try:
@@ -712,6 +725,7 @@ async def list_extensions(client: SupabaseClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def list_policies(client: SupabaseClient, table: str | None = None) -> str:
"""List RLS policies"""
try:
@@ -730,6 +744,7 @@ async def list_policies(client: SupabaseClient, table: str | None = None) -> str
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def list_roles(client: SupabaseClient) -> str:
"""List database roles"""
try:
@@ -747,6 +762,7 @@ async def list_roles(client: SupabaseClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def list_triggers(client: SupabaseClient, table: str | None = None) -> str:
"""List database triggers"""
try:
@@ -765,6 +781,7 @@ async def list_triggers(client: SupabaseClient, table: str | None = None) -> str
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def list_functions(
client: SupabaseClient, schema: str = "public", limit: int = 50, offset: int = 0
) -> str:
@@ -794,6 +811,7 @@ async def list_functions(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def execute_sql(client: SupabaseClient, query: str) -> str:
"""Execute raw SQL query"""
try:
@@ -811,6 +829,7 @@ async def execute_sql(client: SupabaseClient, query: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_table_indexes(client: SupabaseClient, table: str, schema: str = "public") -> str:
"""Get indexes for a table"""
try:
@@ -843,6 +862,7 @@ async def get_table_indexes(client: SupabaseClient, table: str, schema: str = "p
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_table_constraints(client: SupabaseClient, table: str, schema: str = "public") -> str:
"""Get constraints for a table"""
try:
@@ -877,6 +897,7 @@ async def get_table_constraints(client: SupabaseClient, table: str, schema: str
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_table_relationships(
client: SupabaseClient, table: str, schema: str = "public"
) -> str:
diff --git a/plugins/supabase/handlers/functions.py b/plugins/supabase/handlers/functions.py
index 74fe50a..0e20abc 100644
--- a/plugins/supabase/handlers/functions.py
+++ b/plugins/supabase/handlers/functions.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.supabase.client import SupabaseClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (8 tools)"""
return [
@@ -144,10 +145,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Functions Operations (8 tools)
# =====================
+
async def invoke_function(
client: SupabaseClient,
function_name: str,
@@ -166,6 +169,7 @@ async def invoke_function(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def invoke_function_get(
client: SupabaseClient, function_name: str, params: dict | None = None
) -> str:
@@ -193,6 +197,7 @@ async def invoke_function_get(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def list_edge_functions(client: SupabaseClient) -> str:
"""List deployed Edge Functions"""
try:
@@ -223,6 +228,7 @@ async def list_edge_functions(client: SupabaseClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_function_info(client: SupabaseClient, function_name: str) -> str:
"""Get function information"""
try:
@@ -251,6 +257,7 @@ async def get_function_info(client: SupabaseClient, function_name: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def test_function(
client: SupabaseClient, function_name: str, test_data: dict | None = None, method: str = "POST"
) -> str:
@@ -291,6 +298,7 @@ async def test_function(
ensure_ascii=False,
)
+
async def get_function_url(client: SupabaseClient, function_name: str) -> str:
"""Get the full URL for a function"""
try:
@@ -309,6 +317,7 @@ async def get_function_url(client: SupabaseClient, function_name: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def check_function_health(client: SupabaseClient, function_name: str) -> str:
"""Check if a function is healthy"""
try:
@@ -347,6 +356,7 @@ async def check_function_health(client: SupabaseClient, function_name: str) -> s
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def invoke_function_batch(
client: SupabaseClient, function_name: str, payloads: list[dict]
) -> str:
diff --git a/plugins/supabase/handlers/storage.py b/plugins/supabase/handlers/storage.py
index 0080edd..fc32a97 100644
--- a/plugins/supabase/handlers/storage.py
+++ b/plugins/supabase/handlers/storage.py
@@ -6,6 +6,7 @@ from typing import Any
from plugins.supabase.client import SupabaseClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (12 tools)"""
return [
@@ -230,10 +231,12 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
# =====================
# Storage Operations (12 tools)
# =====================
+
async def list_buckets(client: SupabaseClient) -> str:
"""List all storage buckets"""
try:
@@ -251,6 +254,7 @@ async def list_buckets(client: SupabaseClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_bucket(client: SupabaseClient, bucket_id: str) -> str:
"""Get bucket details"""
try:
@@ -260,6 +264,7 @@ async def get_bucket(client: SupabaseClient, bucket_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def create_bucket(
client: SupabaseClient,
name: str,
@@ -284,6 +289,7 @@ async def create_bucket(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def update_bucket(
client: SupabaseClient,
bucket_id: str,
@@ -312,6 +318,7 @@ async def update_bucket(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_bucket(client: SupabaseClient, bucket_id: str) -> str:
"""Delete a bucket"""
try:
@@ -325,6 +332,7 @@ async def delete_bucket(client: SupabaseClient, bucket_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def empty_bucket(client: SupabaseClient, bucket_id: str) -> str:
"""Empty a bucket (delete all files)"""
try:
@@ -338,6 +346,7 @@ async def empty_bucket(client: SupabaseClient, bucket_id: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def list_files(
client: SupabaseClient, bucket: str, path: str = "", limit: int = 100, offset: int = 0
) -> str:
@@ -359,6 +368,7 @@ async def list_files(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def upload_file(
client: SupabaseClient,
bucket: str,
@@ -399,6 +409,7 @@ async def upload_file(
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def download_file(client: SupabaseClient, bucket: str, path: str) -> str:
"""Download a file (returns base64)"""
try:
@@ -426,6 +437,7 @@ async def download_file(client: SupabaseClient, bucket: str, path: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def delete_files(client: SupabaseClient, bucket: str, paths: list[str]) -> str:
"""Delete files from bucket"""
try:
@@ -444,6 +456,7 @@ async def delete_files(client: SupabaseClient, bucket: str, paths: list[str]) ->
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def move_file(client: SupabaseClient, bucket: str, from_path: str, to_path: str) -> str:
"""Move/rename a file"""
try:
@@ -464,6 +477,7 @@ async def move_file(client: SupabaseClient, bucket: str, from_path: str, to_path
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_public_url(client: SupabaseClient, bucket: str, path: str) -> str:
"""Get public URL for a file"""
try:
diff --git a/plugins/supabase/handlers/system.py b/plugins/supabase/handlers/system.py
index 5040af5..2fc6e50 100644
--- a/plugins/supabase/handlers/system.py
+++ b/plugins/supabase/handlers/system.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.supabase.client import SupabaseClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (6 tools)"""
return [
@@ -62,6 +63,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
async def health_check(client: SupabaseClient) -> str:
"""Check health of all Supabase services"""
try:
@@ -84,6 +86,7 @@ async def health_check(client: SupabaseClient) -> str:
ensure_ascii=False,
)
+
async def get_service_status(client: SupabaseClient, service: str) -> str:
"""Get status of a specific service"""
try:
@@ -142,6 +145,7 @@ async def get_service_status(client: SupabaseClient, service: str) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_database_stats(client: SupabaseClient) -> str:
"""Get database statistics"""
try:
@@ -199,6 +203,7 @@ async def get_database_stats(client: SupabaseClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_storage_stats(client: SupabaseClient) -> str:
"""Get storage statistics"""
try:
@@ -243,6 +248,7 @@ async def get_storage_stats(client: SupabaseClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_auth_stats(client: SupabaseClient) -> str:
"""Get authentication statistics"""
try:
@@ -294,6 +300,7 @@ async def get_auth_stats(client: SupabaseClient) -> str:
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
+
async def get_instance_info(client: SupabaseClient) -> str:
"""Get Supabase instance information"""
try:
diff --git a/plugins/supabase/plugin.py b/plugins/supabase/plugin.py
index 7357f1a..53b5b38 100644
--- a/plugins/supabase/plugin.py
+++ b/plugins/supabase/plugin.py
@@ -14,6 +14,7 @@ from plugins.base import BasePlugin
from plugins.supabase import handlers
from plugins.supabase.client import SupabaseClient
+
class SupabasePlugin(BasePlugin):
"""
Supabase Self-Hosted Plugin - Complete backend management.
diff --git a/plugins/woocommerce/plugin.py b/plugins/woocommerce/plugin.py
index 87637ac..481ddee 100644
--- a/plugins/woocommerce/plugin.py
+++ b/plugins/woocommerce/plugin.py
@@ -24,6 +24,7 @@ from plugins.wordpress.handlers import (
get_reports_specs,
)
+
class WooCommercePlugin(BasePlugin):
"""
WooCommerce E-commerce Plugin.
diff --git a/plugins/wordpress/client.py b/plugins/wordpress/client.py
index 4231f9f..2da6c1c 100644
--- a/plugins/wordpress/client.py
+++ b/plugins/wordpress/client.py
@@ -12,16 +12,19 @@ from typing import Any
import aiohttp
+
class ConfigurationError(Exception):
"""Raised when site configuration is invalid or incomplete."""
pass
+
class AuthenticationError(Exception):
"""Raised when authentication fails (401/403)."""
pass
+
class WordPressClient:
"""
WordPress REST API client for HTTP communication.
diff --git a/plugins/wordpress/handlers/comments.py b/plugins/wordpress/handlers/comments.py
index 8ee499c..a780f4f 100644
--- a/plugins/wordpress/handlers/comments.py
+++ b/plugins/wordpress/handlers/comments.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -154,6 +155,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class CommentsHandler:
"""Handle comment-related operations for WordPress"""
diff --git a/plugins/wordpress/handlers/coupons.py b/plugins/wordpress/handlers/coupons.py
index 7ef4612..401409f 100644
--- a/plugins/wordpress/handlers/coupons.py
+++ b/plugins/wordpress/handlers/coupons.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -219,6 +220,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class CouponsHandler:
"""Handle coupon-related operations for WooCommerce"""
diff --git a/plugins/wordpress/handlers/customers.py b/plugins/wordpress/handlers/customers.py
index 8ad2342..30f1233 100644
--- a/plugins/wordpress/handlers/customers.py
+++ b/plugins/wordpress/handlers/customers.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -141,6 +142,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class CustomersHandler:
"""Handle WooCommerce customer operations"""
diff --git a/plugins/wordpress/handlers/media.py b/plugins/wordpress/handlers/media.py
index 515e15d..988caeb 100644
--- a/plugins/wordpress/handlers/media.py
+++ b/plugins/wordpress/handlers/media.py
@@ -7,6 +7,7 @@ import aiohttp
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -155,6 +156,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class MediaHandler:
"""Handle media-related operations for WordPress"""
diff --git a/plugins/wordpress/handlers/menus.py b/plugins/wordpress/handlers/menus.py
index 64f8223..5da28c6 100644
--- a/plugins/wordpress/handlers/menus.py
+++ b/plugins/wordpress/handlers/menus.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -148,6 +149,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class MenusHandler:
"""Handle menu-related operations for WordPress"""
diff --git a/plugins/wordpress/handlers/orders.py b/plugins/wordpress/handlers/orders.py
index aa70be0..1b384c9 100644
--- a/plugins/wordpress/handlers/orders.py
+++ b/plugins/wordpress/handlers/orders.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -196,6 +197,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class OrdersHandler:
"""Handle WooCommerce order-related operations"""
diff --git a/plugins/wordpress/handlers/posts.py b/plugins/wordpress/handlers/posts.py
index 6759323..ef7438a 100644
--- a/plugins/wordpress/handlers/posts.py
+++ b/plugins/wordpress/handlers/posts.py
@@ -7,12 +7,14 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def _count_words(html_content: str) -> int:
"""Strip HTML tags and count words."""
text = re.sub(r"<[^>]+>", " ", html_content)
text = re.sub(r"\s+", " ", text).strip()
return len(text.split()) if text else 0
+
def _strip_html(html_content: str, max_chars: int = 500) -> str:
"""Strip HTML tags and return first max_chars characters."""
text = re.sub(r"<[^>]+>", " ", html_content)
@@ -21,6 +23,7 @@ def _strip_html(html_content: str, max_chars: int = 500) -> str:
return text[:max_chars] + "..."
return text
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -454,6 +457,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class PostsHandler:
"""Handle post-related operations for WordPress"""
diff --git a/plugins/wordpress/handlers/products.py b/plugins/wordpress/handlers/products.py
index 1351539..2ae8a22 100644
--- a/plugins/wordpress/handlers/products.py
+++ b/plugins/wordpress/handlers/products.py
@@ -7,12 +7,14 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def _count_words(html_content: str) -> int:
"""Strip HTML tags and count words."""
text = re.sub(r"<[^>]+>", " ", html_content)
text = re.sub(r"\s+", " ", text).strip()
return len(text.split()) if text else 0
+
def _strip_html(html_content: str, max_chars: int = 500) -> str:
"""Strip HTML tags and return first max_chars characters."""
text = re.sub(r"<[^>]+>", " ", html_content)
@@ -21,6 +23,7 @@ def _strip_html(html_content: str, max_chars: int = 500) -> str:
return text[:max_chars] + "..."
return text
+
def normalize_id_list(value: Any, field_name: str = "item") -> list[dict[str, Any]]:
"""
Convert various formats of category/tag IDs to WooCommerce API format.
@@ -100,6 +103,7 @@ def normalize_id_list(value: Any, field_name: str = "item") -> list[dict[str, An
return result
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -589,6 +593,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class ProductsHandler:
"""Handle WooCommerce product-related operations"""
diff --git a/plugins/wordpress/handlers/reports.py b/plugins/wordpress/handlers/reports.py
index 4e77f2b..3fd6882 100644
--- a/plugins/wordpress/handlers/reports.py
+++ b/plugins/wordpress/handlers/reports.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -86,6 +87,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class ReportsHandler:
"""Handle WooCommerce reporting operations"""
diff --git a/plugins/wordpress/handlers/seo.py b/plugins/wordpress/handlers/seo.py
index 4afa6ef..8b74093 100644
--- a/plugins/wordpress/handlers/seo.py
+++ b/plugins/wordpress/handlers/seo.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -163,6 +164,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class SEOHandler:
"""Handle SEO-related operations for WordPress (Yoast SEO and RankMath)"""
diff --git a/plugins/wordpress/handlers/site.py b/plugins/wordpress/handlers/site.py
index b471b14..cd7d434 100644
--- a/plugins/wordpress/handlers/site.py
+++ b/plugins/wordpress/handlers/site.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -69,6 +70,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class SiteHandler:
"""Handle site management operations for WordPress"""
diff --git a/plugins/wordpress/handlers/taxonomy.py b/plugins/wordpress/handlers/taxonomy.py
index 810a0ef..72acdd6 100644
--- a/plugins/wordpress/handlers/taxonomy.py
+++ b/plugins/wordpress/handlers/taxonomy.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -269,6 +270,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class TaxonomyHandler:
"""Handle taxonomy-related operations for WordPress"""
diff --git a/plugins/wordpress/handlers/users.py b/plugins/wordpress/handlers/users.py
index baf1c48..4eead7d 100644
--- a/plugins/wordpress/handlers/users.py
+++ b/plugins/wordpress/handlers/users.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -46,6 +47,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class UsersHandler:
"""Handle user-related operations for WordPress"""
diff --git a/plugins/wordpress/handlers/wp_cli.py b/plugins/wordpress/handlers/wp_cli.py
index 88516d6..3266125 100644
--- a/plugins/wordpress/handlers/wp_cli.py
+++ b/plugins/wordpress/handlers/wp_cli.py
@@ -5,6 +5,7 @@ from typing import Any
from plugins.wordpress.wp_cli import WPCLIManager
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -183,6 +184,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class WPCLIHandler:
"""Handle WP-CLI operations for WordPress"""
diff --git a/plugins/wordpress/plugin.py b/plugins/wordpress/plugin.py
index e983640..8d638d3 100644
--- a/plugins/wordpress/plugin.py
+++ b/plugins/wordpress/plugin.py
@@ -13,6 +13,7 @@ from plugins.base import BasePlugin
from plugins.wordpress import handlers
from plugins.wordpress.client import WordPressClient
+
class WordPressPlugin(BasePlugin):
"""
WordPress project plugin - Option B architecture.
diff --git a/plugins/wordpress/plugin_old_backup.py b/plugins/wordpress/plugin_old_backup.py
index ae4f437..dd8afbf 100644
--- a/plugins/wordpress/plugin_old_backup.py
+++ b/plugins/wordpress/plugin_old_backup.py
@@ -14,6 +14,7 @@ import aiohttp
from plugins.base import BasePlugin
from plugins.wordpress.wp_cli import WPCLIManager
+
class WordPressPlugin(BasePlugin):
"""
WordPress project plugin.
diff --git a/plugins/wordpress/schemas/common.py b/plugins/wordpress/schemas/common.py
index 857c486..14ed38c 100644
--- a/plugins/wordpress/schemas/common.py
+++ b/plugins/wordpress/schemas/common.py
@@ -8,6 +8,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
+
class PaginationParams(BaseModel):
"""Pagination parameters for list endpoints"""
@@ -16,6 +17,7 @@ class PaginationParams(BaseModel):
model_config = ConfigDict(extra="forbid")
+
class StatusFilter(BaseModel):
"""Status filter for posts/pages/products"""
@@ -29,6 +31,7 @@ class StatusFilter(BaseModel):
raise ValueError(f"Status must be one of: {', '.join(allowed)}")
return v
+
class ErrorResponse(BaseModel):
"""Standard error response"""
@@ -37,6 +40,7 @@ class ErrorResponse(BaseModel):
code: str | None = Field(None, description="Error code")
details: dict[str, Any] | None = Field(None, description="Additional error details")
+
class SuccessResponse(BaseModel):
"""Standard success response"""
diff --git a/plugins/wordpress/schemas/media.py b/plugins/wordpress/schemas/media.py
index f3da132..1be2d43 100644
--- a/plugins/wordpress/schemas/media.py
+++ b/plugins/wordpress/schemas/media.py
@@ -6,6 +6,7 @@ Validation schemas for WordPress media library operations.
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator
+
class MediaBase(BaseModel):
"""Base media schema"""
@@ -16,6 +17,7 @@ class MediaBase(BaseModel):
caption: str | None = Field(None, description="Media caption")
description: str | None = Field(None, description="Media description")
+
class MediaUpload(MediaBase):
"""Schema for uploading media from URL"""
@@ -33,12 +35,14 @@ class MediaUpload(MediaBase):
raise ValueError("Filename too long (max 255 characters)")
return v
+
class MediaUpdate(MediaBase):
"""Schema for updating media metadata"""
# All fields optional for updates
pass
+
class MediaResponse(BaseModel):
"""Schema for media response data"""
diff --git a/plugins/wordpress/schemas/order.py b/plugins/wordpress/schemas/order.py
index f70899b..9bfddf9 100644
--- a/plugins/wordpress/schemas/order.py
+++ b/plugins/wordpress/schemas/order.py
@@ -8,6 +8,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
+
class OrderLineItem(BaseModel):
"""Schema for order line item"""
@@ -19,6 +20,7 @@ class OrderLineItem(BaseModel):
subtotal: str | None = Field(None, description="Line subtotal")
total: str | None = Field(None, description="Line total")
+
class ShippingAddress(BaseModel):
"""Schema for shipping address"""
@@ -34,12 +36,14 @@ class ShippingAddress(BaseModel):
postcode: str | None = Field(None, description="Postal code")
country: str | None = Field(None, description="Country code (ISO 3166-1 alpha-2)")
+
class BillingAddress(ShippingAddress):
"""Schema for billing address (extends shipping)"""
email: EmailStr | None = Field(None, description="Email address")
phone: str | None = Field(None, description="Phone number")
+
class OrderBase(BaseModel):
"""Base order schema"""
@@ -76,6 +80,7 @@ class OrderBase(BaseModel):
raise ValueError(f"Status must be one of: {', '.join(allowed)}")
return v
+
class OrderCreate(OrderBase):
"""Schema for creating a new order"""
@@ -85,12 +90,14 @@ class OrderCreate(OrderBase):
model_config = ConfigDict(extra="allow")
+
class OrderUpdate(OrderBase):
"""Schema for updating an existing order"""
# All fields optional for updates
pass
+
class OrderStatusUpdate(BaseModel):
"""Schema for updating order status"""
@@ -112,6 +119,7 @@ class OrderStatusUpdate(BaseModel):
raise ValueError(f"Status must be one of: {', '.join(allowed)}")
return v
+
class OrderResponse(BaseModel):
"""Schema for order response data"""
@@ -126,6 +134,7 @@ class OrderResponse(BaseModel):
shipping: dict[str, Any]
line_items: list[dict[str, Any]]
+
class CustomerBase(BaseModel):
"""Base customer schema"""
@@ -138,6 +147,7 @@ class CustomerBase(BaseModel):
billing: BillingAddress | None = Field(None, description="Billing address")
shipping: ShippingAddress | None = Field(None, description="Shipping address")
+
class CustomerCreate(CustomerBase):
"""Schema for creating a new customer"""
@@ -145,6 +155,7 @@ class CustomerCreate(CustomerBase):
model_config = ConfigDict(extra="allow")
+
class CustomerUpdate(CustomerBase):
"""Schema for updating an existing customer"""
diff --git a/plugins/wordpress/schemas/post.py b/plugins/wordpress/schemas/post.py
index 08c6f2b..1d5b930 100644
--- a/plugins/wordpress/schemas/post.py
+++ b/plugins/wordpress/schemas/post.py
@@ -6,6 +6,7 @@ Validation schemas for WordPress posts, pages, and custom post types.
from pydantic import BaseModel, ConfigDict, Field, field_validator
+
class PostBase(BaseModel):
"""Base post schema with common fields"""
@@ -44,6 +45,7 @@ class PostBase(BaseModel):
raise ValueError("Status must be 'open' or 'closed'")
return v
+
class PostCreate(PostBase):
"""Schema for creating a new post"""
@@ -52,12 +54,14 @@ class PostCreate(PostBase):
model_config = ConfigDict(extra="allow")
+
class PostUpdate(PostBase):
"""Schema for updating an existing post"""
# All fields optional for updates
pass
+
class PostResponse(BaseModel):
"""Schema for post response data"""
@@ -77,6 +81,7 @@ class PostResponse(BaseModel):
categories: list[int]
tags: list[int]
+
class PageCreate(PostCreate):
"""Schema for creating a new page"""
@@ -84,6 +89,7 @@ class PageCreate(PostCreate):
menu_order: int | None = Field(None, description="Menu order")
template: str | None = Field(None, description="Page template")
+
class PageUpdate(PostUpdate):
"""Schema for updating an existing page"""
@@ -91,11 +97,13 @@ class PageUpdate(PostUpdate):
menu_order: int | None = Field(None, description="Menu order")
template: str | None = Field(None, description="Page template")
+
class CustomPostCreate(PostCreate):
"""Schema for creating custom post types"""
post_type: str = Field(..., description="Custom post type")
+
class CustomPostUpdate(PostUpdate):
"""Schema for updating custom post types"""
diff --git a/plugins/wordpress/schemas/product.py b/plugins/wordpress/schemas/product.py
index 5e335b3..ec05a3f 100644
--- a/plugins/wordpress/schemas/product.py
+++ b/plugins/wordpress/schemas/product.py
@@ -8,6 +8,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
+
class ProductBase(BaseModel):
"""Base product schema"""
@@ -71,18 +72,21 @@ class ProductBase(BaseModel):
raise ValueError(f"Stock status must be one of: {', '.join(allowed)}")
return v
+
class ProductCreate(ProductBase):
"""Schema for creating a new product"""
name: str = Field(..., min_length=1, description="Product name (required)")
type: str = Field("simple", description="Product type")
+
class ProductUpdate(ProductBase):
"""Schema for updating an existing product"""
# All fields optional for updates
pass
+
class ProductResponse(BaseModel):
"""Schema for product response data"""
@@ -100,6 +104,7 @@ class ProductResponse(BaseModel):
stock_status: str
permalink: str
+
class ProductCategory(BaseModel):
"""Schema for product category"""
@@ -112,6 +117,7 @@ class ProductCategory(BaseModel):
display: str | None = Field("default", description="Display type")
image: dict[str, Any] | None = Field(None, description="Category image")
+
class ProductVariation(BaseModel):
"""Schema for product variation"""
@@ -128,6 +134,7 @@ class ProductVariation(BaseModel):
attributes: list[dict[str, Any]] | None = Field(None, description="Variation attributes")
image: dict[str, Any] | None = Field(None, description="Variation image")
+
class ProductAttribute(BaseModel):
"""Schema for product attribute"""
diff --git a/plugins/wordpress/schemas/seo.py b/plugins/wordpress/schemas/seo.py
index 039fa33..966c4e3 100644
--- a/plugins/wordpress/schemas/seo.py
+++ b/plugins/wordpress/schemas/seo.py
@@ -6,6 +6,7 @@ Validation schemas for SEO plugin data (Yoast, RankMath, etc.).
from pydantic import BaseModel, ConfigDict, Field, field_validator
+
class SEOData(BaseModel):
"""Schema for SEO data (read)"""
@@ -23,6 +24,7 @@ class SEOData(BaseModel):
twitter_image: str | None = Field(None, description="Twitter card image URL")
robots: list[str] | None = Field(None, description="Robots meta tags")
+
class SEOUpdate(BaseModel):
"""Schema for SEO data updates"""
@@ -61,6 +63,7 @@ class SEOUpdate(BaseModel):
pass
return v
+
class YoastSEO(SEOData):
"""Yoast SEO specific data"""
@@ -70,6 +73,7 @@ class YoastSEO(SEOData):
yoast_wpseo_metadesc: str | None = Field(None, description="Yoast meta description")
yoast_wpseo_title: str | None = Field(None, description="Yoast SEO title")
+
class RankMathSEO(SEOData):
"""RankMath SEO specific data"""
diff --git a/plugins/wordpress/wp_cli.py b/plugins/wordpress/wp_cli.py
index 4864415..c07d4a0 100644
--- a/plugins/wordpress/wp_cli.py
+++ b/plugins/wordpress/wp_cli.py
@@ -20,6 +20,7 @@ import json
import logging
from typing import Any
+
class WPCLIManager:
"""
Manages WP-CLI command execution for WordPress containers.
diff --git a/plugins/wordpress_advanced/handlers/bulk.py b/plugins/wordpress_advanced/handlers/bulk.py
index a81a42f..c1f8dc7 100644
--- a/plugins/wordpress_advanced/handlers/bulk.py
+++ b/plugins/wordpress_advanced/handlers/bulk.py
@@ -18,6 +18,7 @@ from plugins.wordpress_advanced.schemas.bulk import (
BulkOperationResult,
)
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -292,6 +293,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class BulkHandler:
"""Handles WordPress bulk operations"""
diff --git a/plugins/wordpress_advanced/handlers/database.py b/plugins/wordpress_advanced/handlers/database.py
index 1318e25..1b29f53 100644
--- a/plugins/wordpress_advanced/handlers/database.py
+++ b/plugins/wordpress_advanced/handlers/database.py
@@ -16,6 +16,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
from plugins.wordpress.wp_cli import WPCLIManager
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -169,6 +170,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class DatabaseHandler:
"""Handles WordPress database operations"""
diff --git a/plugins/wordpress_advanced/handlers/system.py b/plugins/wordpress_advanced/handlers/system.py
index f5b0cc8..2e74266 100644
--- a/plugins/wordpress_advanced/handlers/system.py
+++ b/plugins/wordpress_advanced/handlers/system.py
@@ -18,6 +18,7 @@ from typing import Any
from plugins.wordpress.client import WordPressClient
from plugins.wordpress.wp_cli import WPCLIManager
+
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
@@ -111,6 +112,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
]
+
class SystemHandler:
"""Handles WordPress system operations"""
diff --git a/plugins/wordpress_advanced/plugin.py b/plugins/wordpress_advanced/plugin.py
index 51f9745..c9b5696 100644
--- a/plugins/wordpress_advanced/plugin.py
+++ b/plugins/wordpress_advanced/plugin.py
@@ -17,6 +17,7 @@ from plugins.base import BasePlugin
from plugins.wordpress.client import WordPressClient
from plugins.wordpress_advanced import handlers
+
class WordPressAdvancedPlugin(BasePlugin):
"""
WordPress Advanced plugin - separated for security and visibility.
diff --git a/plugins/wordpress_advanced/schemas/bulk.py b/plugins/wordpress_advanced/schemas/bulk.py
index f404306..fa83fde 100644
--- a/plugins/wordpress_advanced/schemas/bulk.py
+++ b/plugins/wordpress_advanced/schemas/bulk.py
@@ -11,6 +11,7 @@ from typing import Any
from pydantic import BaseModel, Field, field_validator
+
class BulkUpdatePostsParams(BaseModel):
"""Parameters for bulk updating posts"""
@@ -55,6 +56,7 @@ class BulkUpdatePostsParams(BaseModel):
return v
+
class BulkDeletePostsParams(BaseModel):
"""Parameters for bulk deleting posts"""
@@ -71,6 +73,7 @@ class BulkDeletePostsParams(BaseModel):
raise ValueError("All post IDs must be positive integers")
return v
+
class BulkUpdateProductsParams(BaseModel):
"""Parameters for bulk updating WooCommerce products"""
@@ -120,6 +123,7 @@ class BulkUpdateProductsParams(BaseModel):
return v
+
class BulkDeleteProductsParams(BaseModel):
"""Parameters for bulk deleting products"""
@@ -136,6 +140,7 @@ class BulkDeleteProductsParams(BaseModel):
raise ValueError("All product IDs must be positive integers")
return v
+
class BulkAssignCategoriesParams(BaseModel):
"""Parameters for bulk assigning categories"""
@@ -164,6 +169,7 @@ class BulkAssignCategoriesParams(BaseModel):
raise ValueError("item_type must be 'post' or 'product'")
return v
+
class BulkAssignTagsParams(BaseModel):
"""Parameters for bulk assigning tags"""
@@ -192,6 +198,7 @@ class BulkAssignTagsParams(BaseModel):
raise ValueError("item_type must be 'post' or 'product'")
return v
+
class BulkUpdateMediaParams(BaseModel):
"""Parameters for bulk updating media items"""
@@ -222,6 +229,7 @@ class BulkUpdateMediaParams(BaseModel):
return v
+
class BulkDeleteMediaParams(BaseModel):
"""Parameters for bulk deleting media items"""
@@ -240,6 +248,7 @@ class BulkDeleteMediaParams(BaseModel):
raise ValueError("All media IDs must be positive integers")
return v
+
class BulkOperationResult(BaseModel):
"""Result of a bulk operation"""
diff --git a/plugins/wordpress_advanced/schemas/database.py b/plugins/wordpress_advanced/schemas/database.py
index fdde271..44914b6 100644
--- a/plugins/wordpress_advanced/schemas/database.py
+++ b/plugins/wordpress_advanced/schemas/database.py
@@ -12,6 +12,7 @@ from typing import Any
from pydantic import BaseModel, Field, field_validator
+
class DatabaseExportParams(BaseModel):
"""Parameters for database export"""
@@ -33,6 +34,7 @@ class DatabaseExportParams(BaseModel):
raise ValueError(f"Invalid table name: {table}")
return v
+
class DatabaseImportParams(BaseModel):
"""Parameters for database import"""
@@ -50,6 +52,7 @@ class DatabaseImportParams(BaseModel):
raise ValueError("URL must start with http:// or https://")
return v
+
class DatabaseBackupParams(BaseModel):
"""Parameters for creating database backup"""
@@ -61,6 +64,7 @@ class DatabaseBackupParams(BaseModel):
default=False, description="Also backup wp-content/uploads directory"
)
+
class DatabaseRestoreParams(BaseModel):
"""Parameters for restoring database"""
@@ -78,6 +82,7 @@ class DatabaseRestoreParams(BaseModel):
raise ValueError("Confirmation required: set confirm=true")
return v
+
class DatabaseSearchParams(BaseModel):
"""Parameters for searching database"""
@@ -91,6 +96,7 @@ class DatabaseSearchParams(BaseModel):
case_sensitive: bool = Field(default=False, description="Case-sensitive search")
max_results: int = Field(default=100, ge=1, le=1000, description="Maximum results to return")
+
class DatabaseQueryParams(BaseModel):
"""Parameters for executing SQL query"""
@@ -136,6 +142,7 @@ class DatabaseQueryParams(BaseModel):
return v
+
class DatabaseSizeResponse(BaseModel):
"""Response model for database size information"""
@@ -143,6 +150,7 @@ class DatabaseSizeResponse(BaseModel):
tables: list[dict[str, Any]] = Field(description="List of tables with size information")
row_count: int = Field(description="Total row count across all tables")
+
class DatabaseTableInfo(BaseModel):
"""Information about a database table"""
@@ -154,6 +162,7 @@ class DatabaseTableInfo(BaseModel):
total_size_mb: float
collation: str
+
class DatabaseRepairResult(BaseModel):
"""Result of database repair operation"""
@@ -161,6 +170,7 @@ class DatabaseRepairResult(BaseModel):
status: str # 'OK', 'Repaired', 'Failed'
message: str | None = None
+
class DatabaseBackupInfo(BaseModel):
"""Information about a database backup"""
diff --git a/plugins/wordpress_advanced/schemas/system.py b/plugins/wordpress_advanced/schemas/system.py
index 64888d6..3fad2d4 100644
--- a/plugins/wordpress_advanced/schemas/system.py
+++ b/plugins/wordpress_advanced/schemas/system.py
@@ -13,6 +13,7 @@ from typing import Any
from pydantic import BaseModel, Field, field_validator
+
class SystemInfoResponse(BaseModel):
"""Comprehensive system information"""
@@ -32,6 +33,7 @@ class SystemInfoResponse(BaseModel):
active_plugins: int = Field(description="Number of active plugins")
active_theme: str = Field(description="Active theme name")
+
class PHPInfoResponse(BaseModel):
"""PHP configuration details"""
@@ -41,6 +43,7 @@ class PHPInfoResponse(BaseModel):
ini_settings: dict[str, str] = Field(description="Important php.ini settings")
disabled_functions: list[str] = Field(default=[], description="Disabled PHP functions")
+
class DiskUsageResponse(BaseModel):
"""Disk usage statistics"""
@@ -55,6 +58,7 @@ class DiskUsageResponse(BaseModel):
)
breakdown: dict[str, float] = Field(default={}, description="Detailed breakdown by directory")
+
class CronEvent(BaseModel):
"""WordPress cron event information"""
@@ -66,6 +70,7 @@ class CronEvent(BaseModel):
)
args: list[Any] = Field(default=[], description="Arguments passed to the hook")
+
class CronListResponse(BaseModel):
"""List of all cron events"""
@@ -73,6 +78,7 @@ class CronListResponse(BaseModel):
total: int = Field(description="Total number of events")
schedules: dict[str, dict[str, Any]] = Field(default={}, description="Available cron schedules")
+
class CronRunParams(BaseModel):
"""Parameters for manually running a cron job"""
@@ -90,6 +96,7 @@ class CronRunParams(BaseModel):
)
return v
+
class ErrorLogParams(BaseModel):
"""Parameters for retrieving error log"""
@@ -111,6 +118,7 @@ class ErrorLogParams(BaseModel):
raise ValueError("level must be: error, warning, notice, or fatal")
return v.lower() if v else v
+
class ErrorLogEntry(BaseModel):
"""Single error log entry"""
@@ -120,6 +128,7 @@ class ErrorLogEntry(BaseModel):
file: str | None = Field(default=None, description="File where error occurred")
line: int | None = Field(default=None, description="Line number")
+
class ErrorLogResponse(BaseModel):
"""Error log retrieval response"""
@@ -128,6 +137,7 @@ class ErrorLogResponse(BaseModel):
filtered_lines: int = Field(description="Number of entries returned")
log_size_mb: float = Field(description="Log file size in MB")
+
class CacheStats(BaseModel):
"""Cache statistics"""
diff --git a/server.py b/server.py
index 1f72ea6..2a7f65c 100644
--- a/server.py
+++ b/server.py
@@ -143,6 +143,7 @@ _dcr_rate_limits: dict = (
{}
) # {ip: {"minute": count, "hour": count, "minute_reset": timestamp, "hour_reset": timestamp}}
+
def is_redirect_uri_allowed_for_open_dcr(redirect_uris: list) -> bool:
"""
Check if all redirect_uris match the allowlist patterns.
@@ -166,6 +167,7 @@ def is_redirect_uri_allowed_for_open_dcr(redirect_uris: list) -> bool:
return False
return True
+
def check_dcr_rate_limit(client_ip: str) -> tuple[bool, str]:
"""
Check if DCR request is within rate limits.
@@ -215,6 +217,7 @@ def check_dcr_rate_limit(client_ip: str) -> tuple[bool, str]:
return True, ""
+
# Validate auth mode
if OAUTH_AUTH_MODE not in ["required", "optional", "trusted_domains"]:
logger.warning(f"Invalid OAUTH_AUTH_MODE '{OAUTH_AUTH_MODE}'. Defaulting to 'trusted_domains'")
@@ -282,9 +285,11 @@ for site_info in site_manager.list_all_sites():
logger.info("=" * 60)
+
# === MCP INSTRUCTIONS HELPER ===
# Phase K.2: Auto-discovery of available sites for AI assistants
+
def generate_mcp_instructions(plugin_type: str = None, site_locked: str = None) -> str:
"""
Generate MCP server instructions for AI assistants.
@@ -421,12 +426,15 @@ Use get_endpoints() to see all available MCP endpoints.
If working with a single site, use it directly without asking the user."""
+
# Set instructions for admin endpoint (after sites are discovered)
mcp.instructions = generate_mcp_instructions()
logger.info("Admin MCP instructions configured with site discovery")
+
# === AUTHENTICATION MIDDLEWARE ===
+
def extract_project_from_tool(tool_name: str) -> str:
"""
Extract project_id from tool name.
@@ -444,6 +452,7 @@ def extract_project_from_tool(tool_name: str) -> str:
# The actual project will be validated when the tool is executed
return "*"
+
def extract_plugin_type_from_tool(tool_name: str) -> str | None:
"""
Extract plugin type from tool name for tool visibility filtering.
@@ -491,6 +500,7 @@ def extract_plugin_type_from_tool(tool_name: str) -> str | None:
# System tools (no plugin type)
return None
+
def check_tool_visibility(tool_name: str, api_key_project_id: str) -> bool:
"""
Check if API key has visibility to the requested tool.
@@ -553,6 +563,7 @@ def check_tool_visibility(tool_name: str, api_key_project_id: str) -> bool:
# Check if plugin types match
return tool_plugin_type == key_plugin_type
+
def determine_required_scope(tool_name: str) -> str:
"""
Determine required scope for a tool.
@@ -580,6 +591,7 @@ def determine_required_scope(tool_name: str) -> str:
# Everything else is read
return "read"
+
class UserAuthMiddleware(Middleware):
"""
Middleware to enforce Bearer token authentication for all MCP tool calls.
@@ -830,12 +842,15 @@ class UserAuthMiddleware(Middleware):
logger.error(f"Authentication error: {e}", exc_info=True)
raise ToolError(f"Authentication error: {str(e)}")
+
# Add authentication middleware to MCP server
mcp.add_middleware(UserAuthMiddleware())
logger.info("Authentication middleware enabled")
+
# === AUDIT LOGGING MIDDLEWARE ===
+
class AuditLoggingMiddleware(Middleware):
"""
Middleware to log all tool calls for audit purposes.
@@ -918,16 +933,19 @@ class AuditLoggingMiddleware(Middleware):
# Don't let logging errors break the tool call
logger.error(f"Failed to log audit entry: {log_error}", exc_info=True)
+
# Add audit logging middleware to MCP server
mcp.add_middleware(AuditLoggingMiddleware())
logger.info("Audit logging middleware enabled")
+
# === RATE LIMITING (Phase 7.3) ===
# Initialize rate limiter
rate_limiter = get_rate_limiter()
logger.info("Rate limiter initialized (Phase 7.3)")
+
class RateLimitMiddleware(Middleware):
"""
Middleware to enforce rate limiting for all tool calls (Phase 7.3).
@@ -1011,10 +1029,12 @@ class RateLimitMiddleware(Middleware):
# Rate limit check passed - proceed with tool execution
return await call_next(context)
+
# Add rate limiting middleware to MCP server
mcp.add_middleware(RateLimitMiddleware())
logger.info("Rate limiting middleware enabled (Phase 7.3)")
+
# === HEALTH MONITORING (Phase 7.2) ===
from core import initialize_health_monitor
@@ -1028,6 +1048,7 @@ health_monitor = initialize_health_monitor(
)
logger.info("Health monitor initialized (Phase 7.2)")
+
class HealthMetricsMiddleware(Middleware):
"""
Middleware to track health metrics for all tool calls (Phase 7.2).
@@ -1116,12 +1137,15 @@ class HealthMetricsMiddleware(Middleware):
# Don't let metrics tracking errors break the tool call
logger.error(f"Failed to record health metric: {metric_error}", exc_info=True)
+
# Add health metrics middleware
mcp.add_middleware(HealthMetricsMiddleware())
logger.info("Health metrics middleware enabled (Phase 7.2)")
+
# === ENDPOINT MIDDLEWARE HELPER ===
+
def add_endpoint_middleware(endpoint_mcp, endpoint_name: str = "unknown"):
"""
Add authentication, audit, and rate limiting middleware to an endpoint.
@@ -1138,8 +1162,10 @@ def add_endpoint_middleware(endpoint_mcp, endpoint_name: str = "unknown"):
endpoint_mcp.add_middleware(RateLimitMiddleware())
logger.debug(f"Added auth/audit/rate-limit middleware to {endpoint_name} endpoint")
+
# === SYSTEM TOOLS ===
+
# Internal implementation functions (not decorated) for system tools
async def _list_projects_impl() -> str:
"""Internal implementation for listing projects."""
@@ -1153,6 +1179,7 @@ async def _list_projects_impl() -> str:
logger.error(f"Error listing projects: {e}", exc_info=True)
return f"Error: {str(e)}"
+
@mcp.tool()
async def list_projects() -> str:
"""
@@ -1166,6 +1193,7 @@ async def list_projects() -> str:
"""
return await _list_projects_impl()
+
# Phase K.2.3: Helper for site discovery in plugin endpoints
async def _list_sites_impl(plugin_type: str) -> str:
"""
@@ -1204,6 +1232,7 @@ async def _list_sites_impl(plugin_type: str) -> str:
{"error": str(e), "message": f"Failed to list {plugin_type} sites"}, indent=2
)
+
@mcp.tool()
async def get_project_info(project_id: str) -> str:
"""
@@ -1228,6 +1257,7 @@ async def get_project_info(project_id: str) -> str:
logger.error(f"Error getting project info: {e}", exc_info=True)
return f"Error: {str(e)}"
+
@mcp.tool()
async def check_all_projects_health() -> str:
"""
@@ -1253,6 +1283,7 @@ async def check_all_projects_health() -> str:
logger.error(f"Error checking health: {e}", exc_info=True)
return f"Error: {str(e)}"
+
@mcp.tool()
async def get_project_health(project_id: str) -> str:
"""
@@ -1279,6 +1310,7 @@ async def get_project_health(project_id: str) -> str:
logger.error(f"Error getting project health: {e}", exc_info=True)
return f"Error: {str(e)}"
+
@mcp.tool()
async def get_system_metrics() -> str:
"""
@@ -1304,6 +1336,7 @@ async def get_system_metrics() -> str:
logger.error(f"Error getting system metrics: {e}", exc_info=True)
return f"Error: {str(e)}"
+
@mcp.tool()
async def get_system_uptime() -> str:
"""
@@ -1322,6 +1355,7 @@ async def get_system_uptime() -> str:
logger.error(f"Error getting uptime: {e}", exc_info=True)
return f"Error: {str(e)}"
+
@mcp.tool()
async def get_project_metrics(project_id: str, hours: int = 1) -> str:
"""
@@ -1351,6 +1385,7 @@ async def get_project_metrics(project_id: str, hours: int = 1) -> str:
logger.error(f"Error getting project metrics: {e}", exc_info=True)
return f"Error: {str(e)}"
+
@mcp.tool()
async def export_health_metrics(output_path: str = "logs/metrics_export.json") -> str:
"""
@@ -1369,6 +1404,7 @@ async def export_health_metrics(output_path: str = "logs/metrics_export.json") -
logger.error(f"Error exporting metrics: {e}", exc_info=True)
return f"Error: {str(e)}"
+
# Internal implementations for rate limiting tools
async def _get_rate_limit_stats_impl(client_id: str = None) -> str:
"""Internal implementation for getting rate limit stats."""
@@ -1387,6 +1423,7 @@ async def _get_rate_limit_stats_impl(client_id: str = None) -> str:
logger.error(f"Error getting rate limit stats: {e}", exc_info=True)
return f"Error: {str(e)}"
+
async def _reset_rate_limit_impl(client_id: str = None) -> str:
"""Internal implementation for resetting rate limit."""
try:
@@ -1403,6 +1440,7 @@ async def _reset_rate_limit_impl(client_id: str = None) -> str:
logger.error(f"Error resetting rate limit: {e}", exc_info=True)
return f"Error: {str(e)}"
+
@mcp.tool()
async def get_rate_limit_stats(client_id: str = None) -> str:
"""
@@ -1417,6 +1455,7 @@ async def get_rate_limit_stats(client_id: str = None) -> str:
"""
return await _get_rate_limit_stats_impl(client_id)
+
@mcp.tool()
async def reset_rate_limit(client_id: str = None) -> str:
"""
@@ -1433,8 +1472,10 @@ async def reset_rate_limit(client_id: str = None) -> str:
"""
return await _reset_rate_limit_impl(client_id)
+
# === DYNAMIC TOOL REGISTRATION ===
+
def create_dynamic_tool(name: str, description: str, handler, input_schema: dict):
"""
Create a dynamic tool wrapper that works with FastMCP's decorator pattern.
@@ -1533,6 +1574,7 @@ async def {name}({param_str}):
return dynamic_wrapper
+
def register_project_tools():
"""
Dynamically register all project tools from plugins.
@@ -1767,9 +1809,11 @@ def register_project_tools():
return total_tools
+
# Register all project tools at startup
_total_tool_count = register_project_tools()
+
# === OAUTH 2.1 ENDPOINTS ===
from urllib.parse import urlencode
@@ -1778,6 +1822,7 @@ from core.oauth import OAuthError, get_oauth_server
oauth_server = get_oauth_server()
+
def get_oauth_base_url(request: Request) -> str:
"""
Get the correct base URL for OAuth endpoints.
@@ -1800,6 +1845,7 @@ def get_oauth_base_url(request: Request) -> str:
return str(request.base_url).rstrip("/")
+
@mcp.custom_route("/.well-known/oauth-authorization-server", methods=["GET"])
async def oauth_metadata(request: Request) -> JSONResponse:
"""
@@ -1850,6 +1896,7 @@ async def oauth_metadata(request: Request) -> JSONResponse:
return JSONResponse(metadata)
+
@mcp.custom_route("/mcp/.well-known/oauth-authorization-server", methods=["GET"])
async def oauth_metadata_mcp_path(request: Request) -> JSONResponse:
"""
@@ -1860,6 +1907,7 @@ async def oauth_metadata_mcp_path(request: Request) -> JSONResponse:
"""
return await oauth_metadata(request)
+
@mcp.custom_route("/.well-known/oauth-protected-resource", methods=["GET"])
async def oauth_protected_resource(request: Request) -> JSONResponse:
"""
@@ -1880,6 +1928,7 @@ async def oauth_protected_resource(request: Request) -> JSONResponse:
return JSONResponse(metadata)
+
async def oauth_protected_resource_path(request: Request) -> JSONResponse:
"""
OAuth 2.0 Protected Resource Metadata for path-specific requests (RFC 9728)
@@ -1901,6 +1950,7 @@ async def oauth_protected_resource_path(request: Request) -> JSONResponse:
return JSONResponse(metadata)
+
@mcp.custom_route("/oauth/register", methods=["POST"])
async def oauth_register(request: Request) -> JSONResponse:
"""
@@ -2128,6 +2178,7 @@ async def oauth_register(request: Request) -> JSONResponse:
logger.error(f"Error in dynamic client registration: {e}", exc_info=True)
return JSONResponse({"error": "server_error", "error_description": str(e)}, status_code=500)
+
@mcp.custom_route("/oauth/authorize", methods=["GET"])
async def oauth_authorize(request: Request):
"""
@@ -2268,6 +2319,7 @@ async def oauth_authorize(request: Request):
status_code=500,
)
+
@mcp.custom_route("/oauth/authorize/confirm", methods=["POST"])
async def oauth_authorize_confirm(request: Request):
"""
@@ -2458,6 +2510,7 @@ async def oauth_authorize_confirm(request: Request):
status_code=500,
)
+
@mcp.custom_route("/oauth/token", methods=["POST"])
async def oauth_token(request: Request) -> JSONResponse:
"""
@@ -2567,8 +2620,10 @@ async def oauth_token(request: Request) -> JSONResponse:
logger.error(f"OAuth token error: {e}", exc_info=True)
return JSONResponse({"error": "server_error", "error_description": str(e)}, status_code=500)
+
# === OAUTH CLIENT MANAGEMENT TOOLS ===
+
# Internal implementation for OAuth register
async def _oauth_register_client_impl(
client_name: str,
@@ -2606,6 +2661,7 @@ async def _oauth_register_client_impl(
logger.error(f"Error registering OAuth client: {e}", exc_info=True)
raise ToolError(f"Failed to register client: {e}")
+
@mcp.tool()
async def oauth_register_client(
client_name: str,
@@ -2631,6 +2687,7 @@ async def oauth_register_client(
client_name, redirect_uris, grant_types, allowed_scopes, metadata
)
+
# Internal implementations for OAuth tools
async def _oauth_list_clients_impl() -> dict:
"""Internal implementation for listing OAuth clients."""
@@ -2656,6 +2713,7 @@ async def _oauth_list_clients_impl() -> dict:
logger.error(f"Error listing OAuth clients: {e}", exc_info=True)
raise ToolError(f"Failed to list clients: {e}")
+
async def _oauth_revoke_client_impl(client_id: str) -> dict:
"""Internal implementation for revoking OAuth client."""
from core.oauth import get_client_registry
@@ -2682,6 +2740,7 @@ async def _oauth_revoke_client_impl(client_id: str) -> dict:
logger.error(f"Error revoking OAuth client: {e}", exc_info=True)
raise ToolError(f"Failed to revoke client: {e}")
+
async def _oauth_get_client_info_impl(client_id: str) -> dict:
"""Internal implementation for getting OAuth client info."""
from core.oauth import get_client_registry
@@ -2707,6 +2766,7 @@ async def _oauth_get_client_info_impl(client_id: str) -> dict:
logger.error(f"Error getting OAuth client info: {e}", exc_info=True)
return {"success": False, "error": str(e)}
+
@mcp.tool()
async def oauth_list_clients() -> dict:
"""
@@ -2719,6 +2779,7 @@ async def oauth_list_clients() -> dict:
"""
return await _oauth_list_clients_impl()
+
@mcp.tool()
async def oauth_revoke_client(client_id: str) -> dict:
"""
@@ -2735,6 +2796,7 @@ async def oauth_revoke_client(client_id: str) -> dict:
"""
return await _oauth_revoke_client_impl(client_id)
+
@mcp.tool()
async def oauth_get_client_info(client_id: str) -> dict:
"""
@@ -2748,8 +2810,10 @@ async def oauth_get_client_info(client_id: str) -> dict:
"""
return await _oauth_get_client_info_impl(client_id)
+
# === PHASE X.3: SYSTEM TOOLS ===
+
# Internal implementations for Phase X.3 system tools
async def _get_endpoints_impl() -> dict:
"""Internal implementation for listing endpoints."""
@@ -2862,6 +2926,7 @@ async def _get_endpoints_impl() -> dict:
logger.error(f"Error listing endpoints: {e}", exc_info=True)
return {"success": False, "error": str(e)}
+
async def _get_system_info_impl() -> dict:
"""Internal implementation for getting system info."""
try:
@@ -2900,6 +2965,7 @@ async def _get_system_info_impl() -> dict:
logger.error(f"Error getting system info: {e}", exc_info=True)
return {"success": False, "error": str(e)}
+
async def _get_audit_log_impl(
limit: int = 50, event_type: str = None, project_id: str = None
) -> dict:
@@ -2930,6 +2996,7 @@ async def _get_audit_log_impl(
logger.error(f"Error getting audit log: {e}", exc_info=True)
return {"success": False, "error": str(e)}
+
async def _set_rate_limit_config_impl(
requests_per_minute: int = None, requests_per_hour: int = None, requests_per_day: int = None
) -> dict:
@@ -2961,21 +3028,25 @@ async def _set_rate_limit_config_impl(
logger.error(f"Error setting rate limit config: {e}", exc_info=True)
return {"success": False, "error": str(e)}
+
@mcp.tool()
async def get_endpoints() -> dict:
"""List all available MCP endpoints (Phase X.3)."""
return await _get_endpoints_impl()
+
@mcp.tool()
async def get_system_info() -> dict:
"""Get comprehensive system information (Phase X.3)."""
return await _get_system_info_impl()
+
@mcp.tool()
async def get_audit_log(limit: int = 50, event_type: str = None, project_id: str = None) -> dict:
"""Get audit log entries (Phase X.3)."""
return await _get_audit_log_impl(limit, event_type, project_id)
+
@mcp.tool()
async def set_rate_limit_config(
requests_per_minute: int = None, requests_per_hour: int = None, requests_per_day: int = None
@@ -2985,11 +3056,13 @@ async def set_rate_limit_config(
requests_per_minute, requests_per_hour, requests_per_day
)
+
# === HEALTH CHECK ENDPOINT ===
# Track server start time for uptime calculation
server_start_time = time.time()
+
@mcp.custom_route("/health", methods=["GET"])
async def health_check(request: Request) -> JSONResponse:
"""
@@ -3019,8 +3092,10 @@ async def health_check(request: Request) -> JSONResponse:
}
)
+
# === API KEYS MANAGEMENT TOOLS ===
+
# Internal implementation functions (not decorated)
async def _manage_api_keys_create_impl(
project_id: str, scope: str = "read", expires_in_days: int = None, description: str = None
@@ -3051,6 +3126,7 @@ async def _manage_api_keys_create_impl(
logger.error(f"Error creating API key: {e}")
return {"success": False, "error": str(e)}
+
async def _manage_api_keys_list_impl(project_id: str = None, include_revoked: bool = False) -> dict:
"""Internal implementation for listing API keys."""
try:
@@ -3060,6 +3136,7 @@ async def _manage_api_keys_list_impl(project_id: str = None, include_revoked: bo
logger.error(f"Error listing API keys: {e}")
return {"success": False, "error": str(e)}
+
async def _manage_api_keys_get_info_impl(key_id: str) -> dict:
"""Internal implementation for getting API key info."""
try:
@@ -3071,6 +3148,7 @@ async def _manage_api_keys_get_info_impl(key_id: str) -> dict:
logger.error(f"Error getting key info: {e}")
return {"success": False, "error": str(e)}
+
async def _manage_api_keys_revoke_impl(key_id: str) -> dict:
"""Internal implementation for revoking API key."""
try:
@@ -3082,6 +3160,7 @@ async def _manage_api_keys_revoke_impl(key_id: str) -> dict:
logger.error(f"Error revoking API key: {e}")
return {"success": False, "error": str(e)}
+
async def _manage_api_keys_delete_impl(key_id: str) -> dict:
"""Internal implementation for deleting API key."""
try:
@@ -3093,6 +3172,7 @@ async def _manage_api_keys_delete_impl(key_id: str) -> dict:
logger.error(f"Error deleting API key: {e}")
return {"success": False, "error": str(e)}
+
async def _manage_api_keys_rotate_impl(project_id: str) -> dict:
"""Internal implementation for rotating API keys."""
try:
@@ -3108,6 +3188,7 @@ async def _manage_api_keys_rotate_impl(project_id: str) -> dict:
logger.error(f"Error rotating API keys: {e}")
return {"success": False, "error": str(e)}
+
@mcp.tool()
async def manage_api_keys_create(
project_id: str, scope: str = "read", expires_in_days: int = None, description: str = None
@@ -3137,6 +3218,7 @@ async def manage_api_keys_create(
"""
return await _manage_api_keys_create_impl(project_id, scope, expires_in_days, description)
+
@mcp.tool()
async def manage_api_keys_list(project_id: str = None, include_revoked: bool = False) -> dict:
"""
@@ -3171,6 +3253,7 @@ async def manage_api_keys_list(project_id: str = None, include_revoked: bool = F
"""
return await _manage_api_keys_list_impl(project_id, include_revoked)
+
@mcp.tool()
async def manage_api_keys_get_info(key_id: str) -> dict:
"""
@@ -3184,6 +3267,7 @@ async def manage_api_keys_get_info(key_id: str) -> dict:
"""
return await _manage_api_keys_get_info_impl(key_id)
+
@mcp.tool()
async def manage_api_keys_revoke(key_id: str) -> dict:
"""
@@ -3197,6 +3281,7 @@ async def manage_api_keys_revoke(key_id: str) -> dict:
"""
return await _manage_api_keys_revoke_impl(key_id)
+
@mcp.tool()
async def manage_api_keys_delete(key_id: str) -> dict:
"""
@@ -3210,6 +3295,7 @@ async def manage_api_keys_delete(key_id: str) -> dict:
"""
return await _manage_api_keys_delete_impl(key_id)
+
@mcp.tool()
async def manage_api_keys_rotate(project_id: str) -> dict:
"""
@@ -3226,10 +3312,12 @@ async def manage_api_keys_rotate(project_id: str) -> dict:
"""
return await _manage_api_keys_rotate_impl(project_id)
+
# === SERVER STARTUP ===
# Create separate MCP instances for each endpoint
+
def create_system_mcp():
"""
Create System-only MCP instance (Phase X.3).
@@ -3362,6 +3450,7 @@ Use get_endpoints() to see all available MCP endpoints."""
logger.info("Created System endpoint with 17 tools")
return system_mcp
+
def create_wordpress_mcp():
"""Create WordPress-only MCP instance"""
from fastmcp import FastMCP
@@ -3410,6 +3499,7 @@ def create_wordpress_mcp():
logger.info(f"Created WordPress endpoint with {count} tools (including list_sites)")
return wp_mcp
+
def create_wordpress_advanced_mcp():
"""Create WordPress Advanced MCP instance"""
from fastmcp import FastMCP
@@ -3453,6 +3543,7 @@ def create_wordpress_advanced_mcp():
logger.info(f"Created WordPress Advanced endpoint with {count} tools (including list_sites)")
return wp_adv_mcp
+
def create_woocommerce_mcp():
"""Create WooCommerce-only MCP instance (Phase D.1)"""
from fastmcp import FastMCP
@@ -3494,6 +3585,7 @@ def create_woocommerce_mcp():
logger.info(f"Created WooCommerce endpoint with {count} tools (including list_sites)")
return woo_mcp
+
def create_gitea_mcp():
"""Create Gitea-only MCP instance"""
from fastmcp import FastMCP
@@ -3535,6 +3627,7 @@ def create_gitea_mcp():
logger.info(f"Created Gitea endpoint with {count} tools (including list_sites)")
return gitea_mcp
+
def create_openpanel_mcp():
"""Create OpenPanel-only MCP instance"""
from fastmcp import FastMCP
@@ -3576,6 +3669,7 @@ def create_openpanel_mcp():
logger.info(f"Created OpenPanel endpoint with {count} tools (including list_sites)")
return openpanel_mcp
+
def create_n8n_mcp():
"""Create n8n-only MCP instance"""
from fastmcp import FastMCP
@@ -3617,6 +3711,7 @@ def create_n8n_mcp():
logger.info(f"Created n8n endpoint with {count} tools (including list_sites)")
return n8n_mcp
+
def create_supabase_mcp():
"""Create Supabase-only MCP instance"""
from fastmcp import FastMCP
@@ -3658,6 +3753,7 @@ def create_supabase_mcp():
logger.info(f"Created Supabase endpoint with {count} tools (including list_sites)")
return supabase_mcp
+
def create_appwrite_mcp():
"""Create Appwrite-only MCP instance"""
from fastmcp import FastMCP
@@ -3699,6 +3795,7 @@ def create_appwrite_mcp():
logger.info(f"Created Appwrite endpoint with {count} tools (including list_sites)")
return appwrite_mcp
+
def create_directus_mcp():
"""Create Directus-only MCP instance"""
from fastmcp import FastMCP
@@ -3740,6 +3837,7 @@ def create_directus_mcp():
logger.info(f"Created Directus endpoint with {count} tools (including list_sites)")
return directus_mcp
+
def create_project_mcp(project_id: str, plugin_type: str, site_id: str, alias: str = None):
"""
Create MCP instance for a specific project with site-locked tools.
@@ -3830,6 +3928,7 @@ def create_project_mcp(project_id: str, plugin_type: str, site_id: str, alias: s
logger.info(f"Created Project endpoint for {project_id} with {count} tools")
return project_mcp
+
def create_per_project_endpoints():
"""
Create MCP endpoints for each discovered site.
@@ -3873,6 +3972,7 @@ def create_per_project_endpoints():
logger.info(f"Created {len(project_endpoints)} per-project endpoints")
return project_endpoints
+
def create_multi_endpoint_app(transport: str = "streamable-http"):
"""Create Starlette app with multiple MCP endpoints"""
from contextlib import asynccontextmanager
@@ -4205,6 +4305,7 @@ def create_multi_endpoint_app(transport: str = "streamable-http"):
return app
+
def main():
"""Main entry point for the MCP server."""
import argparse
@@ -4262,5 +4363,6 @@ def main():
logger.error(f"Server error: {e}", exc_info=True)
sys.exit(1)
+
if __name__ == "__main__":
main()
diff --git a/server_multi.py b/server_multi.py
index bd2642c..c62f5be 100644
--- a/server_multi.py
+++ b/server_multi.py
@@ -114,6 +114,7 @@ DCR_RATE_LIMIT_PER_MINUTE = int(os.getenv("DCR_RATE_LIMIT_PER_MINUTE", "10"))
DCR_RATE_LIMIT_PER_HOUR = int(os.getenv("DCR_RATE_LIMIT_PER_HOUR", "30"))
_dcr_rate_limits: dict = {}
+
def is_redirect_uri_allowed_for_open_dcr(redirect_uris: list) -> bool:
"""Check if all redirect_uris match the allowlist patterns."""
for uri in redirect_uris:
@@ -126,6 +127,7 @@ def is_redirect_uri_allowed_for_open_dcr(redirect_uris: list) -> bool:
return False
return True
+
def check_dcr_rate_limit(client_ip: str) -> tuple:
"""Check if DCR request is within rate limits."""
now = time.time()
@@ -151,6 +153,7 @@ def check_dcr_rate_limit(client_ip: str) -> tuple:
limits["hour"] += 1
return True, ""
+
# Initialize managers
auth_manager = get_auth_manager()
api_key_manager = get_api_key_manager()
@@ -193,8 +196,10 @@ oauth_server = get_oauth_server()
# Server start time
server_start_time = time.time()
+
# === TOOL GENERATION ===
+
def generate_all_tools():
"""Generate tools from all plugins into the tool registry"""
logger.info("=" * 60)
@@ -248,8 +253,10 @@ def generate_all_tools():
logger.info(f"Total tools in registry: {tool_registry.get_count()}")
logger.info("=" * 60)
+
# === ENDPOINT CREATION ===
+
class EndpointAuthMiddleware(Middleware):
"""Authentication middleware for specific endpoint"""
@@ -379,6 +386,7 @@ class EndpointAuthMiddleware(Middleware):
logger.error(f"Auth error in {tool_name}: {e}")
raise ToolError(f"Authentication error: {str(e)}")
+
def create_dynamic_tool(
name: str, description: str, handler: Callable, input_schema: dict | None = None
) -> Callable:
@@ -440,6 +448,7 @@ async def {name}({param_str}):
return dynamic_wrapper
+
def get_tools_for_endpoint(config: EndpointConfig) -> list[ToolDefinition]:
"""Get tools that should be registered for a specific endpoint"""
tools = []
@@ -472,6 +481,7 @@ def get_tools_for_endpoint(config: EndpointConfig) -> list[ToolDefinition]:
return tools
+
def create_mcp_endpoint(config: EndpointConfig) -> FastMCP:
"""Create a FastMCP instance for a specific endpoint"""
mcp = FastMCP(config.name)
@@ -496,8 +506,10 @@ def create_mcp_endpoint(config: EndpointConfig) -> FastMCP:
return mcp
+
# === SYSTEM TOOLS (added to admin endpoint) ===
+
def add_system_tools(mcp: FastMCP):
"""Add system tools to an MCP instance"""
@@ -663,8 +675,10 @@ def add_system_tools(mcp: FastMCP):
except Exception as e:
return {"success": False, "error": str(e)}
+
# === HTTP ROUTES ===
+
async def health_check(request: Request) -> JSONResponse:
"""Health check endpoint"""
return JSONResponse(
@@ -678,6 +692,7 @@ async def health_check(request: Request) -> JSONResponse:
}
)
+
async def endpoint_info(request: Request) -> JSONResponse:
"""List all available endpoints including per-project endpoints"""
endpoints = []
@@ -742,6 +757,7 @@ async def endpoint_info(request: Request) -> JSONResponse:
}
)
+
# OAuth endpoints (imported from server.py patterns)
async def oauth_metadata(request: Request) -> JSONResponse:
"""OAuth 2.0 Authorization Server Metadata (RFC 8414)"""
@@ -760,6 +776,7 @@ async def oauth_metadata(request: Request) -> JSONResponse:
}
)
+
async def oauth_protected_resource(request: Request) -> JSONResponse:
"""OAuth 2.0 Protected Resource Metadata (RFC 9728)"""
base_url = str(request.base_url).rstrip("/")
@@ -772,6 +789,7 @@ async def oauth_protected_resource(request: Request) -> JSONResponse:
}
)
+
async def oauth_register(request: Request) -> JSONResponse:
"""
Dynamic Client Registration (RFC 7591) - MCP Spec Compliant
@@ -865,6 +883,7 @@ async def oauth_register(request: Request) -> JSONResponse:
logger.error(f"DCR error: {e}")
return JSONResponse({"error": "server_error", "error_description": str(e)}, status_code=400)
+
async def oauth_authorize(request: Request):
"""OAuth Authorization Endpoint"""
client_id = request.query_params.get("client_id")
@@ -902,6 +921,7 @@ async def oauth_authorize(request: Request):
},
)
+
async def oauth_authorize_confirm(request: Request):
"""Handle OAuth authorization form submission"""
form = await request.form()
@@ -941,6 +961,7 @@ async def oauth_authorize_confirm(request: Request):
except Exception as e:
return HTMLResponse(f"