From c73e39f6e4824617e38cc340b9204cfcec9f909d Mon Sep 17 00:00:00 2001 From: airano Date: Tue, 17 Feb 2026 18:19:39 +0330 Subject: [PATCH] fix(ci): fix black/ruff failures, add CoC and PR template - Fix Python formatting (sync no longer strips blank lines from .py files) - Remove test_community_build.py (tests private sync module) - Fix ruff warnings in test files - Add CODE_OF_CONDUCT.md - Add .github/PULL_REQUEST_TEMPLATE.md Co-Authored-By: Claude Opus 4.6 --- .github/PULL_REQUEST_TEMPLATE.md | 43 +++ CODE_OF_CONDUCT.md | 40 +++ Dockerfile | 1 + core/api_keys.py | 7 + core/audit_log.py | 5 + core/auth.py | 3 + core/context.py | 3 + core/dashboard/auth.py | 3 + core/dashboard/routes.py | 52 ++++ core/endpoints/config.py | 5 + core/endpoints/factory.py | 1 + core/endpoints/middleware.py | 5 + core/endpoints/registry.py | 5 + core/health.py | 8 + core/i18n.py | 4 + core/oauth/client_registry.py | 3 + core/oauth/csrf.py | 3 + core/oauth/pkce.py | 3 + core/oauth/schemas.py | 6 + core/oauth/server.py | 4 + core/oauth/storage.py | 3 + core/oauth/token_manager.py | 4 + core/project_manager.py | 3 + core/rate_limiter.py | 6 + core/site_manager.py | 4 + core/site_registry.py | 4 + core/tool_generator.py | 3 + core/tool_registry.py | 4 + core/unified_tools.py | 1 + examples/basic_usage.py | 10 + plugins/appwrite/client.py | 1 + plugins/appwrite/handlers/databases.py | 20 ++ plugins/appwrite/handlers/documents.py | 22 ++ plugins/appwrite/handlers/functions.py | 16 ++ plugins/appwrite/handlers/messaging.py | 14 + plugins/appwrite/handlers/storage.py | 16 ++ plugins/appwrite/handlers/system.py | 10 + plugins/appwrite/handlers/teams.py | 12 + plugins/appwrite/handlers/users.py | 14 + plugins/appwrite/plugin.py | 1 + plugins/directus/client.py | 2 + plugins/directus/handlers/access.py | 15 + plugins/directus/handlers/automation.py | 15 + plugins/directus/handlers/collections.py | 17 ++ plugins/directus/handlers/content.py | 13 + plugins/directus/handlers/dashboards.py | 10 + plugins/directus/handlers/files.py | 15 + plugins/directus/handlers/items.py | 15 + plugins/directus/handlers/system.py | 12 + plugins/directus/handlers/users.py | 12 + plugins/directus/plugin.py | 1 + plugins/gitea/client.py | 1 + plugins/gitea/handlers/issues.py | 13 + plugins/gitea/handlers/pull_requests.py | 16 ++ plugins/gitea/handlers/repositories.py | 17 ++ plugins/gitea/handlers/users.py | 9 + plugins/gitea/handlers/webhooks.py | 6 + plugins/gitea/plugin.py | 1 + plugins/gitea/schemas/common.py | 7 + plugins/gitea/schemas/issue.py | 10 + plugins/gitea/schemas/pull_request.py | 11 + plugins/gitea/schemas/repository.py | 10 + plugins/gitea/schemas/user.py | 7 + plugins/gitea/schemas/webhook.py | 5 + plugins/n8n/client.py | 1 + plugins/n8n/handlers/credentials.py | 7 + plugins/n8n/handlers/executions.py | 10 + plugins/n8n/handlers/projects.py | 9 + plugins/n8n/handlers/system.py | 5 + plugins/n8n/handlers/tags.py | 7 + plugins/n8n/handlers/users.py | 6 + plugins/n8n/handlers/variables.py | 7 + plugins/n8n/handlers/workflows.py | 16 ++ plugins/n8n/plugin.py | 1 + plugins/openpanel/client.py | 1 + plugins/openpanel/handlers/clients.py | 8 + plugins/openpanel/handlers/dashboards.py | 12 + plugins/openpanel/handlers/events.py | 12 + plugins/openpanel/handlers/export.py | 12 + plugins/openpanel/handlers/funnels.py | 10 + plugins/openpanel/handlers/profiles.py | 10 + plugins/openpanel/handlers/projects.py | 10 + plugins/openpanel/handlers/reports.py | 10 + plugins/openpanel/handlers/system.py | 8 + plugins/openpanel/handlers/utils.py | 1 + plugins/openpanel/plugin.py | 1 + plugins/supabase/client.py | 1 + plugins/supabase/handlers/admin.py | 14 + plugins/supabase/handlers/auth.py | 16 ++ plugins/supabase/handlers/database.py | 21 ++ plugins/supabase/handlers/functions.py | 10 + plugins/supabase/handlers/storage.py | 14 + plugins/supabase/handlers/system.py | 7 + plugins/supabase/plugin.py | 1 + plugins/woocommerce/plugin.py | 1 + plugins/wordpress/client.py | 3 + plugins/wordpress/handlers/comments.py | 2 + plugins/wordpress/handlers/coupons.py | 2 + plugins/wordpress/handlers/customers.py | 2 + plugins/wordpress/handlers/media.py | 2 + plugins/wordpress/handlers/menus.py | 2 + plugins/wordpress/handlers/orders.py | 2 + plugins/wordpress/handlers/posts.py | 4 + plugins/wordpress/handlers/products.py | 5 + plugins/wordpress/handlers/reports.py | 2 + plugins/wordpress/handlers/seo.py | 2 + plugins/wordpress/handlers/site.py | 2 + plugins/wordpress/handlers/taxonomy.py | 2 + plugins/wordpress/handlers/users.py | 2 + plugins/wordpress/handlers/wp_cli.py | 2 + plugins/wordpress/plugin.py | 1 + plugins/wordpress/plugin_old_backup.py | 1 + plugins/wordpress/schemas/common.py | 4 + plugins/wordpress/schemas/media.py | 4 + plugins/wordpress/schemas/order.py | 11 + plugins/wordpress/schemas/post.py | 8 + plugins/wordpress/schemas/product.py | 7 + plugins/wordpress/schemas/seo.py | 4 + plugins/wordpress/wp_cli.py | 1 + plugins/wordpress_advanced/handlers/bulk.py | 2 + .../wordpress_advanced/handlers/database.py | 2 + plugins/wordpress_advanced/handlers/system.py | 2 + plugins/wordpress_advanced/plugin.py | 1 + plugins/wordpress_advanced/schemas/bulk.py | 9 + .../wordpress_advanced/schemas/database.py | 10 + plugins/wordpress_advanced/schemas/system.py | 10 + server.py | 102 +++++++ server_multi.py | 27 ++ tests/test_auth.py | 3 - tests/test_community_build.py | 270 ------------------ tests/test_dashboard.py | 20 +- tests/test_rate_limiter.py | 2 - tests/test_site_manager.py | 1 - tests/test_woocommerce_plugin.py | 35 ++- tests/test_wordpress_plugin.py | 68 +++-- 135 files changed, 1185 insertions(+), 317 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 tests/test_community_build.py 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"

Error loading health page

{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"

Error: {e}

", status_code=400) + async def oauth_token(request: Request) -> JSONResponse: """OAuth Token Endpoint""" try: @@ -976,8 +997,10 @@ async def oauth_token(request: Request) -> JSONResponse: except Exception as e: return JSONResponse({"error": str(e)}, status_code=500) + # === MAIN APP CREATION === + def create_per_project_endpoints() -> dict[str, FastMCP]: """ Create per-project endpoints for each discovered site. @@ -1027,6 +1050,7 @@ def create_per_project_endpoints() -> dict[str, FastMCP]: logger.info(f"Created {len(project_endpoints)} per-project endpoints") return project_endpoints + def create_app() -> Starlette: """Create the main Starlette application with all endpoints""" from starlette.middleware.base import BaseHTTPMiddleware @@ -1103,8 +1127,10 @@ def create_app() -> Starlette: return app + # === ENTRY POINT === + def main(): """Main entry point""" import argparse @@ -1127,5 +1153,6 @@ def main(): uvicorn.run(app, host=args.host, port=args.port, log_level="info") + if __name__ == "__main__": main() diff --git a/tests/test_auth.py b/tests/test_auth.py index 70439a2..ae099df 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,8 +1,5 @@ """Tests for Authentication System (core/auth.py).""" -import os -import secrets - import pytest from core.auth import AuthManager diff --git a/tests/test_community_build.py b/tests/test_community_build.py deleted file mode 100644 index 1f620c5..0000000 --- a/tests/test_community_build.py +++ /dev/null @@ -1,270 +0,0 @@ -"""Tests for Community Build Sync Script.""" - -import tempfile -from pathlib import Path - -import pytest - -# Import sync module from scripts -import sys - -sys.path.insert(0, str(Path(__file__).parent.parent / "scripts" / "community-build")) -from sync import ( - SyncReport, - apply_branding_transform, - collect_files, - parse_communityignore, - scan_for_secrets, - should_exclude, - should_transform, - sync, -) - - -class TestParseCommunityignore: - """Test .communityignore parsing.""" - - def test_parse_valid_file(self, tmp_path): - ignore_file = tmp_path / ".communityignore" - ignore_file.write_text("# comment\n\nfoo/\n*.pyc\nbar.txt\n") - patterns = parse_communityignore(ignore_file) - assert patterns == ["foo/", "*.pyc", "bar.txt"] - - def test_parse_missing_file(self, tmp_path): - patterns = parse_communityignore(tmp_path / "nonexistent") - assert patterns == [] - - def test_skip_comments_and_empty(self, tmp_path): - ignore_file = tmp_path / ".communityignore" - ignore_file.write_text("# header\n\n# another\nreal_pattern\n\n") - patterns = parse_communityignore(ignore_file) - assert patterns == ["real_pattern"] - - -class TestShouldExclude: - """Test file exclusion logic.""" - - def test_directory_pattern(self): - assert should_exclude("docs/plans/file.md", ["docs/plans/"]) is True - assert should_exclude("docs/other/file.md", ["docs/plans/"]) is False - - def test_exact_match(self): - assert should_exclude("MASTER_CONTEXT.md", ["MASTER_CONTEXT.md"]) is True - assert should_exclude("README.md", ["MASTER_CONTEXT.md"]) is False - - def test_glob_pattern(self): - assert should_exclude("module.pyc", ["*.pyc"]) is True - assert should_exclude("module.py", ["*.pyc"]) is False - - def test_nested_directory(self): - assert should_exclude("__pycache__/module.cpython-311.pyc", ["__pycache__/"]) is True - - def test_no_patterns(self): - assert should_exclude("any/file.txt", []) is False - - def test_component_match(self): - """Pattern matching against directory components.""" - assert should_exclude("deep/__pycache__/file.pyc", ["__pycache__/"]) is True - - -class TestScanForSecrets: - """Test secret detection.""" - - def test_no_secrets(self, tmp_path): - f = tmp_path / "clean.py" - f.write_text("x = 1\nprint('hello')\n") - assert scan_for_secrets(f, "clean.py") == [] - - def test_detects_private_key(self, tmp_path): - f = tmp_path / "bad.pem" - f.write_text("-----BEGIN PRIVATE KEY-----\ndata\n-----END PRIVATE KEY-----\n") - warnings = scan_for_secrets(f, "bad.pem") - assert len(warnings) > 0 - - def test_allowlisted_file_skipped(self, tmp_path): - f = tmp_path / "env.example" - f.write_text('password = "super_secret_value"\n') - assert scan_for_secrets(f, "env.example") == [] - - def test_detects_api_key_pattern(self, tmp_path): - f = tmp_path / "config.py" - f.write_text('api_key = "AKIAIOSFODNN7EXAMPLE12345"\n') - warnings = scan_for_secrets(f, "config.py") - assert len(warnings) > 0 - - -class TestSync: - """Test full sync operation.""" - - @pytest.fixture - def source_tree(self, tmp_path): - """Create a mock source tree.""" - src = tmp_path / "source" - src.mkdir() - - # Create .communityignore - (src / ".communityignore").write_text("secret/\n*.log\nINTERNAL.md\n.communityignore\n") - - # Create files that should be copied - (src / "README.md").write_text("# Public Readme") - (src / "core").mkdir() - (src / "core" / "auth.py").write_text("class AuthManager: pass") - - # Create files that should be excluded - (src / "secret").mkdir() - (src / "secret" / "keys.json").write_text('{"key": "value"}') - (src / "INTERNAL.md").write_text("Internal docs") - (src / "app.log").write_text("log data") - - return src - - def test_dry_run(self, source_tree, tmp_path): - output = tmp_path / "output" - report = sync(source_tree, output, dry_run=True) - - assert report.total_copied > 0 - assert report.total_excluded > 0 - assert not output.exists() # Nothing should be created - - def test_actual_sync(self, source_tree, tmp_path): - output = tmp_path / "output" - report = sync(source_tree, output) - - assert (output / "README.md").exists() - assert (output / "core" / "auth.py").exists() - assert not (output / "secret").exists() - assert not (output / "INTERNAL.md").exists() - assert not (output / "app.log").exists() - - def test_communityignore_excluded(self, source_tree, tmp_path): - output = tmp_path / "output" - report = sync(source_tree, output) - - # .communityignore itself should be excluded - assert not (output / ".communityignore").exists() - assert ".communityignore" in report.excluded - - -class TestSyncReport: - """Test report generation.""" - - def test_summary_format(self): - report = SyncReport( - copied=["a.py", "b.py"], - excluded=["c.log"], - source_dir="/src", - output_dir="/out", - timestamp="2026-02-17 12:00:00 UTC", - ) - summary = report.summary() - assert "Files copied: **2**" in summary - assert "Files excluded: **1**" in summary - - def test_summary_with_warnings(self): - report = SyncReport( - copied=["a.py"], - excluded=[], - secret_warnings=["file.py: found secret"], - source_dir="/src", - output_dir="/out", - timestamp="now", - ) - summary = report.summary() - assert "Secret Warnings" in summary - - def test_summary_has_review_checklist(self): - report = SyncReport( - copied=["a.py"], - excluded=[], - source_dir="/src", - output_dir="/out", - timestamp="now", - ) - summary = report.summary() - assert "Pre-Publish Review Checklist" in summary - assert "APPROVED FOR PUBLISH" in summary - - -class TestBrandingTransform: - """Test branding transform (B.3).""" - - def test_replaces_package_name(self): - content = 'name = "coolify-mcp-hub"' - transformed, changed = apply_branding_transform(content) - assert changed is True - assert 'name = "mcphub"' in transformed - - def test_replaces_display_name(self): - content = "Welcome to Coolify MCP Hub" - transformed, changed = apply_branding_transform(content) - assert "MCP Hub" in transformed - assert "Coolify" not in transformed - - def test_replaces_urls(self): - content = "Homepage: https://airano.ir" - transformed, changed = apply_branding_transform(content) - assert "mcphub.dev" in transformed - assert "airano.ir" not in transformed - - def test_replaces_repo_urls(self): - content = "https://gitea.airano.ir/dev/coolify-mcp-hub" - transformed, changed = apply_branding_transform(content) - assert "github.com/airano-ir/mcphub" in transformed - - def test_replaces_email(self): - content = "Contact: gitea@airano.ir" - transformed, changed = apply_branding_transform(content) - assert "hello@mcphub.dev" in transformed - - def test_strips_private_comments(self): - content = "line1\n# PRIVATE: internal note\nline3\n" - transformed, changed = apply_branding_transform(content) - assert "PRIVATE" not in transformed - assert "line1" in transformed - assert "line3" in transformed - - def test_no_changes_returns_false(self): - content = "clean content with no markers\n" - transformed, changed = apply_branding_transform(content) - assert changed is False - - def test_should_transform_pyproject(self): - assert should_transform("pyproject.toml") is True - - def test_should_transform_readme(self): - assert should_transform("README.md") is True - - def test_should_transform_docs(self): - assert should_transform("docs/OAUTH_GUIDE.md") is True - - def test_should_transform_python(self): - assert should_transform("core/auth.py") is True - assert should_transform("server.py") is True - assert should_transform("plugins/wordpress/plugin.py") is True - - def test_should_not_transform_binary(self): - assert should_transform("core/data.bin") is False - assert should_transform("images/logo.png") is False - - def test_transform_applied_in_sync(self, tmp_path): - """Full sync should apply transforms to matching files.""" - src = tmp_path / "source" - src.mkdir() - (src / ".communityignore").write_text(".communityignore\n") - (src / "README.md").write_text("Welcome to Coolify MCP Hub\nhttps://airano.ir\n") - (src / "core").mkdir() - (src / "core" / "auth.py").write_text("# coolify-mcp-hub internal") - - output = tmp_path / "output" - report = sync(src, output) - - # README.md should be transformed - readme = (output / "README.md").read_text() - assert "MCP Hub" in readme - assert "Coolify" not in readme - assert "mcphub.dev" in readme - assert "README.md" in report.transformed - - # auth.py SHOULD be transformed (Python files now in TRANSFORM_GLOBS) - auth = (output / "core" / "auth.py").read_text() - assert "mcphub" in auth # transformed diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 86e44d7..4be9fdd 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -6,7 +6,6 @@ language detection, translations, and utility functions. import time from datetime import UTC, datetime, timedelta -from unittest.mock import patch import jwt import pytest @@ -20,7 +19,6 @@ from core.dashboard.routes import ( get_translations, ) - # --- Plugin Display Names --- @@ -49,8 +47,15 @@ class TestPluginDisplayNames: def test_all_nine_plugins_mapped(self): """All 9 plugin types should be in the display names map.""" expected = { - "wordpress", "woocommerce", "wordpress_advanced", "gitea", - "n8n", "supabase", "openpanel", "appwrite", "directus", + "wordpress", + "woocommerce", + "wordpress_advanced", + "gitea", + "n8n", + "supabase", + "openpanel", + "appwrite", + "directus", } assert expected == set(PLUGIN_DISPLAY_NAMES.keys()) @@ -119,7 +124,9 @@ class TestTranslations: """English and Farsi should have the same translation keys.""" en_keys = set(DASHBOARD_TRANSLATIONS["en"].keys()) fa_keys = set(DASHBOARD_TRANSLATIONS["fa"].keys()) - assert en_keys == fa_keys, f"Missing keys in fa: {en_keys - fa_keys}, extra in fa: {fa_keys - en_keys}" + assert ( + en_keys == fa_keys + ), f"Missing keys in fa: {en_keys - fa_keys}, extra in fa: {fa_keys - en_keys}" def test_no_empty_translations(self): """No translation value should be empty.""" @@ -197,7 +204,8 @@ class TestDashboardRateLimiting: @pytest.fixture def auth(self): return DashboardAuth( - secret_key="test-secret", master_api_key="sk-master", + secret_key="test-secret", + master_api_key="sk-master", ) def test_within_limit(self, auth): diff --git a/tests/test_rate_limiter.py b/tests/test_rate_limiter.py index 8749ca8..b8d8536 100644 --- a/tests/test_rate_limiter.py +++ b/tests/test_rate_limiter.py @@ -1,7 +1,5 @@ """Tests for Rate Limiter (core/rate_limiter.py).""" -import time - import pytest from core.rate_limiter import ClientRateLimitState, RateLimitConfig, RateLimiter, TokenBucket diff --git a/tests/test_site_manager.py b/tests/test_site_manager.py index 599c4b3..2ae2442 100644 --- a/tests/test_site_manager.py +++ b/tests/test_site_manager.py @@ -4,7 +4,6 @@ import pytest from core.site_manager import SiteConfig, SiteManager - # --- SiteConfig Tests --- diff --git a/tests/test_woocommerce_plugin.py b/tests/test_woocommerce_plugin.py index 6db1dfd..1c2b7d6 100644 --- a/tests/test_woocommerce_plugin.py +++ b/tests/test_woocommerce_plugin.py @@ -11,7 +11,6 @@ import pytest from plugins.base import BasePlugin from plugins.woocommerce.plugin import WooCommercePlugin - # --- WooCommercePlugin Initialization --- @@ -102,7 +101,7 @@ class TestWooCommerceToolSpecs: """Each spec should have name, method_name, description, schema, scope.""" specs = WooCommercePlugin.get_tool_specifications() for spec in specs: - assert "name" in spec, f"Missing 'name' in spec" + assert "name" in spec, "Missing 'name' in spec" assert "method_name" in spec, f"Missing 'method_name' in {spec.get('name')}" assert "description" in spec, f"Missing 'description' in {spec.get('name')}" assert "schema" in spec, f"Missing 'schema' in {spec.get('name')}" @@ -113,26 +112,42 @@ class TestWooCommerceToolSpecs: specs = WooCommercePlugin.get_tool_specifications() valid_scopes = {"read", "write", "admin"} for spec in specs: - assert spec["scope"] in valid_scopes, f"Invalid scope '{spec['scope']}' in {spec['name']}" + assert ( + spec["scope"] in valid_scopes + ), f"Invalid scope '{spec['scope']}' in {spec['name']}" def test_specs_unique_names(self): """All tool names should be unique.""" specs = WooCommercePlugin.get_tool_specifications() names = [s["name"] for s in specs] - assert len(names) == len(set(names)), f"Duplicate names: {[n for n in names if names.count(n) > 1]}" + assert len(names) == len( + set(names) + ), f"Duplicate names: {[n for n in names if names.count(n) > 1]}" def test_product_tools_present(self): """Should include product management tools.""" specs = WooCommercePlugin.get_tool_specifications() names = {s["name"] for s in specs} - expected = {"list_products", "get_product", "create_product", "update_product", "delete_product"} + expected = { + "list_products", + "get_product", + "create_product", + "update_product", + "delete_product", + } assert expected.issubset(names), f"Missing product tools: {expected - names}" def test_order_tools_present(self): """Should include order management tools.""" specs = WooCommercePlugin.get_tool_specifications() names = {s["name"] for s in specs} - expected = {"list_orders", "get_order", "create_order", "update_order_status", "delete_order"} + expected = { + "list_orders", + "get_order", + "create_order", + "update_order_status", + "delete_order", + } assert expected.issubset(names), f"Missing order tools: {expected - names}" def test_customer_tools_present(self): @@ -193,7 +208,7 @@ class TestWooCommerceHandlerDelegation: async def test_create_product_delegates(self, plugin): """create_product should delegate to products handler.""" plugin.products.create_product = AsyncMock(return_value={"id": 99}) - result = await plugin.create_product(name="Widget", regular_price="19.99") + await plugin.create_product(name="Widget", regular_price="19.99") plugin.products.create_product.assert_called_once_with(name="Widget", regular_price="19.99") @pytest.mark.asyncio @@ -207,7 +222,7 @@ class TestWooCommerceHandlerDelegation: async def test_get_order_delegates(self, plugin): """get_order should delegate to orders handler.""" plugin.orders.get_order = AsyncMock(return_value={"id": 1, "status": "completed"}) - result = await plugin.get_order(order_id=1) + await plugin.get_order(order_id=1) plugin.orders.get_order.assert_called_once_with(order_id=1) @pytest.mark.asyncio @@ -221,14 +236,14 @@ class TestWooCommerceHandlerDelegation: async def test_create_coupon_delegates(self, plugin): """create_coupon should delegate to coupons handler.""" plugin.coupons.create_coupon = AsyncMock(return_value={"id": 5}) - result = await plugin.create_coupon(code="SAVE10", discount_type="percent") + await plugin.create_coupon(code="SAVE10", discount_type="percent") plugin.coupons.create_coupon.assert_called_once_with(code="SAVE10", discount_type="percent") @pytest.mark.asyncio async def test_get_sales_report_delegates(self, plugin): """get_sales_report should delegate to reports handler.""" plugin.reports.get_sales_report = AsyncMock(return_value={"total": 1000}) - result = await plugin.get_sales_report(period="month") + await plugin.get_sales_report(period="month") plugin.reports.get_sales_report.assert_called_once_with(period="month") diff --git a/tests/test_wordpress_plugin.py b/tests/test_wordpress_plugin.py index 81ab039..a15622e 100644 --- a/tests/test_wordpress_plugin.py +++ b/tests/test_wordpress_plugin.py @@ -14,7 +14,6 @@ from plugins.base import BasePlugin, PluginRegistry from plugins.wordpress.client import AuthenticationError, ConfigurationError, WordPressClient from plugins.wordpress.plugin import WordPressPlugin - # --- WordPressClient Tests --- @@ -87,7 +86,9 @@ class TestWordPressClientErrorParsing: def test_parse_json_error(self, client): """Should parse JSON error responses.""" - error_text = json.dumps({"code": "rest_forbidden", "message": "Sorry, you are not allowed."}) + error_text = json.dumps( + {"code": "rest_forbidden", "message": "Sorry, you are not allowed."} + ) result = client._parse_error_response(403, error_text) assert result["error_code"] == "ACCESS_DENIED" assert result["status_code"] == 403 @@ -159,10 +160,12 @@ class TestWordPressClientRequest: mock_response.json = AsyncMock(return_value={"id": 1}) mock_session = AsyncMock() - mock_session.request = MagicMock(return_value=AsyncMock( - __aenter__=AsyncMock(return_value=mock_response), - __aexit__=AsyncMock(return_value=False), - )) + mock_session.request = MagicMock( + return_value=AsyncMock( + __aenter__=AsyncMock(return_value=mock_response), + __aexit__=AsyncMock(return_value=False), + ) + ) with patch("aiohttp.ClientSession") as mock_cls: mock_cls.return_value = AsyncMock( @@ -171,7 +174,8 @@ class TestWordPressClientRequest: ) result = await client.request( - "GET", "posts", + "GET", + "posts", params={"status": "publish", "search": None, "tags": "", "ids": []}, ) assert result == {"id": 1} @@ -194,10 +198,12 @@ class TestWordPressClientRequest: ) mock_session = AsyncMock() - mock_session.request = MagicMock(return_value=AsyncMock( - __aenter__=AsyncMock(return_value=mock_response), - __aexit__=AsyncMock(return_value=False), - )) + mock_session.request = MagicMock( + return_value=AsyncMock( + __aenter__=AsyncMock(return_value=mock_response), + __aexit__=AsyncMock(return_value=False), + ) + ) with patch("aiohttp.ClientSession") as mock_cls: mock_cls.return_value = AsyncMock( @@ -216,10 +222,12 @@ class TestWordPressClientRequest: mock_response.text = AsyncMock(return_value="Internal Server Error") mock_session = AsyncMock() - mock_session.request = MagicMock(return_value=AsyncMock( - __aenter__=AsyncMock(return_value=mock_response), - __aexit__=AsyncMock(return_value=False), - )) + mock_session.request = MagicMock( + return_value=AsyncMock( + __aenter__=AsyncMock(return_value=mock_response), + __aexit__=AsyncMock(return_value=False), + ) + ) with patch("aiohttp.ClientSession") as mock_cls: mock_cls.return_value = AsyncMock( @@ -236,8 +244,11 @@ class TestWordPressClientRequest: client.request = AsyncMock(return_value={"posts": []}) result = await client.get("posts", params={"per_page": 10}) client.request.assert_called_once_with( - "GET", "posts", params={"per_page": 10}, - use_custom_namespace=False, use_woocommerce=False, + "GET", + "posts", + params={"per_page": 10}, + use_custom_namespace=False, + use_woocommerce=False, ) assert result == {"posts": []} @@ -263,8 +274,11 @@ class TestWordPressClientRequest: client.request = AsyncMock(return_value={"available": True}) await client.get("products", use_woocommerce=True) client.request.assert_called_once_with( - "GET", "products", params=None, - use_custom_namespace=False, use_woocommerce=True, + "GET", + "products", + params=None, + use_custom_namespace=False, + use_woocommerce=True, ) @@ -361,7 +375,7 @@ class TestWordPressToolSpecifications: """Each spec should have name, method_name, description, schema, scope.""" specs = WordPressPlugin.get_tool_specifications() for spec in specs: - assert "name" in spec, f"Missing 'name' in spec" + assert "name" in spec, "Missing 'name' in spec" assert "method_name" in spec, f"Missing 'method_name' in {spec.get('name')}" assert "description" in spec, f"Missing 'description' in {spec.get('name')}" assert "schema" in spec, f"Missing 'schema' in {spec.get('name')}" @@ -372,13 +386,17 @@ class TestWordPressToolSpecifications: specs = WordPressPlugin.get_tool_specifications() valid_scopes = {"read", "write", "admin"} for spec in specs: - assert spec["scope"] in valid_scopes, f"Invalid scope '{spec['scope']}' in {spec['name']}" + assert ( + spec["scope"] in valid_scopes + ), f"Invalid scope '{spec['scope']}' in {spec['name']}" def test_specs_unique_names(self): """All tool names should be unique.""" specs = WordPressPlugin.get_tool_specifications() names = [s["name"] for s in specs] - assert len(names) == len(set(names)), f"Duplicate tool names found: {[n for n in names if names.count(n) > 1]}" + assert len(names) == len( + set(names) + ), f"Duplicate tool names found: {[n for n in names if names.count(n) > 1]}" def test_core_tools_present(self): """Should include key WordPress tools.""" @@ -442,21 +460,21 @@ class TestWordPressHandlerDelegation: async def test_list_media_delegates(self, plugin): """list_media should delegate to media handler.""" plugin.media.list_media = AsyncMock(return_value=[]) - result = await plugin.list_media() + await plugin.list_media() plugin.media.list_media.assert_called_once() @pytest.mark.asyncio async def test_list_categories_delegates(self, plugin): """list_categories should delegate to taxonomy handler.""" plugin.taxonomy.list_categories = AsyncMock(return_value=[]) - result = await plugin.list_categories() + await plugin.list_categories() plugin.taxonomy.list_categories.assert_called_once() @pytest.mark.asyncio async def test_list_comments_delegates(self, plugin): """list_comments should delegate to comments handler.""" plugin.comments.list_comments = AsyncMock(return_value=[]) - result = await plugin.list_comments() + await plugin.list_comments() plugin.comments.list_comments.assert_called_once() @pytest.mark.asyncio