Initial commit: MCP Hub Community Edition v3.0.0

Community edition generated from private repo via sync pipeline.
Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n,
Supabase, OpenPanel, Appwrite, Directus) with ~587 tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-17 08:34:44 +03:30
commit cf62e65c55
237 changed files with 87596 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
"""Supabase Plugin - Self-Hosted Database & Backend Management"""
from plugins.supabase.client import SupabaseClient
from plugins.supabase.plugin import SupabasePlugin
__all__ = ["SupabasePlugin", "SupabaseClient"]

676
plugins/supabase/client.py Normal file
View File

@@ -0,0 +1,676 @@
"""
Supabase REST API Client (Self-Hosted)
Handles all HTTP communication with Supabase Self-Hosted APIs.
All requests go through Kong API Gateway on single base URL.
APIs:
- PostgREST (/rest/v1/) - Database CRUD
- GoTrue (/auth/v1/) - Authentication
- Storage (/storage/v1/) - File storage
- Edge Functions (/functions/v1/) - Serverless
- postgres-meta (/pg/) - Database admin
"""
import base64
import logging
from typing import Any
import aiohttp
class SupabaseClient:
"""
Supabase Self-Hosted API client.
All requests go through Kong gateway on single base URL.
Uses JWT-based authentication with anon_key or service_role_key.
"""
def __init__(self, base_url: str, anon_key: str, service_role_key: str):
"""
Initialize Supabase API client.
Args:
base_url: Supabase instance URL (Kong gateway)
anon_key: Public API key (RLS protected)
service_role_key: Admin API key (bypasses RLS)
"""
self.base_url = base_url.rstrip("/")
self.anon_key = anon_key
self.service_role_key = service_role_key
# Initialize logger
self.logger = logging.getLogger(f"SupabaseClient.{base_url}")
def _get_headers(
self, use_service_role: bool = False, additional_headers: dict | None = None
) -> dict[str, str]:
"""
Get request headers with API key authentication.
Args:
use_service_role: Use service_role_key (bypasses RLS)
additional_headers: Additional headers to include
Returns:
Dict: Headers with authentication
"""
key = self.service_role_key if use_service_role else self.anon_key
headers = {
"apikey": key,
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"Accept": "application/json",
}
if additional_headers:
headers.update(additional_headers)
return headers
async def request(
self,
method: str,
endpoint: str,
params: dict | None = None,
json_data: dict | None = None,
data: bytes | None = None,
headers_override: dict | None = None,
use_service_role: bool = False,
) -> Any:
"""
Make authenticated request to Supabase API.
Args:
method: HTTP method
endpoint: API endpoint (with leading /)
params: Query parameters
json_data: JSON body data
data: Raw binary data (for file uploads)
headers_override: Override/add headers
use_service_role: Use service_role_key
Returns:
API response
Raises:
Exception: On API errors
"""
url = f"{self.base_url}{endpoint}"
headers = self._get_headers(use_service_role, headers_override)
# Remove Content-Type for binary data
if data is not None:
headers.pop("Content-Type", None)
# Filter None values
if params:
params = {k: v for k, v in params.items() if v is not None}
if json_data:
json_data = {k: v for k, v in json_data.items() if v is not None}
self.logger.debug(f"{method} {url}")
async with aiohttp.ClientSession() as session:
kwargs = {
"method": method,
"url": url,
"headers": headers,
}
if params:
kwargs["params"] = params
if json_data:
kwargs["json"] = json_data
if data:
kwargs["data"] = data
async with session.request(**kwargs) as response:
self.logger.debug(f"Response status: {response.status}")
# Handle 204 No Content
if response.status == 204:
return {"success": True}
# Handle binary responses (file downloads)
content_type = response.headers.get("Content-Type", "")
if "application/json" not in content_type and response.status < 400:
# Return binary data as base64
binary_data = await response.read()
return {
"data": base64.b64encode(binary_data).decode(),
"content_type": content_type,
"size": len(binary_data),
}
# Parse JSON response
try:
response_data = await response.json()
except Exception:
response_text = await response.text()
if response.status >= 400:
raise Exception(
f"Supabase API error (status {response.status}): {response_text}"
)
return {"success": True, "message": response_text}
# Check for errors
if response.status >= 400:
error_msg = self._extract_error_message(response_data)
raise Exception(f"Supabase API error (status {response.status}): {error_msg}")
return response_data
def _extract_error_message(self, response_data: Any) -> str:
"""Extract error message from various response formats."""
if isinstance(response_data, dict):
# PostgREST error format
if "message" in response_data:
return response_data["message"]
# GoTrue error format
if "error_description" in response_data:
return response_data["error_description"]
if "msg" in response_data:
return response_data["msg"]
if "error" in response_data:
return response_data["error"]
return str(response_data)
# =====================
# POSTGREST (Database)
# =====================
async def query_table(
self,
table: str,
select: str = "*",
filters: list[dict] | None = None,
order: str | None = None,
limit: int = 100,
offset: int = 0,
use_service_role: bool = False,
) -> list[dict]:
"""
Query data from a table.
Args:
table: Table name
select: Columns to select
filters: List of filter conditions
order: Order by clause (e.g., "created_at.desc")
limit: Maximum rows
offset: Offset for pagination
"""
params = {"select": select, "limit": limit, "offset": offset}
if order:
params["order"] = order
# Build filter query string
headers = {}
if filters:
for f in filters:
col = f.get("column")
op = f.get("operator", "eq")
val = f.get("value")
if col and val is not None:
params[col] = f"{op}.{val}"
# Request single objects as array
headers["Accept"] = "application/json"
return await self.request(
"GET",
f"/rest/v1/{table}",
params=params,
headers_override=headers,
use_service_role=use_service_role,
)
async def insert_rows(
self,
table: str,
rows: list[dict],
upsert: bool = False,
on_conflict: str | None = None,
use_service_role: bool = False,
) -> list[dict]:
"""Insert rows into a table."""
headers = {"Prefer": "return=representation"}
if upsert:
headers["Prefer"] = "return=representation,resolution=merge-duplicates"
if on_conflict:
headers["on-conflict"] = on_conflict
return await self.request(
"POST",
f"/rest/v1/{table}",
json_data=rows if isinstance(rows, list) else [rows],
headers_override=headers,
use_service_role=use_service_role,
)
async def update_rows(
self, table: str, data: dict, filters: list[dict], use_service_role: bool = False
) -> list[dict]:
"""Update rows matching filters."""
params = {}
for f in filters:
col = f.get("column")
op = f.get("operator", "eq")
val = f.get("value")
if col and val is not None:
params[col] = f"{op}.{val}"
headers = {"Prefer": "return=representation"}
return await self.request(
"PATCH",
f"/rest/v1/{table}",
params=params,
json_data=data,
headers_override=headers,
use_service_role=use_service_role,
)
async def delete_rows(
self, table: str, filters: list[dict], use_service_role: bool = False
) -> list[dict]:
"""Delete rows matching filters."""
params = {}
for f in filters:
col = f.get("column")
op = f.get("operator", "eq")
val = f.get("value")
if col and val is not None:
params[col] = f"{op}.{val}"
headers = {"Prefer": "return=representation"}
return await self.request(
"DELETE",
f"/rest/v1/{table}",
params=params,
headers_override=headers,
use_service_role=use_service_role,
)
async def execute_rpc(
self, function_name: str, params: dict | None = None, use_service_role: bool = False
) -> Any:
"""Execute a stored procedure/function."""
return await self.request(
"POST",
f"/rest/v1/rpc/{function_name}",
json_data=params or {},
use_service_role=use_service_role,
)
async def count_rows(
self, table: str, filters: list[dict] | None = None, use_service_role: bool = False
) -> int:
"""Count rows in a table."""
params = {"select": "count"}
if filters:
for f in filters:
col = f.get("column")
op = f.get("operator", "eq")
val = f.get("value")
if col and val is not None:
params[col] = f"{op}.{val}"
headers = {"Accept": "application/json", "Prefer": "count=exact"}
result = await self.request(
"HEAD",
f"/rest/v1/{table}",
params=params,
headers_override=headers,
use_service_role=use_service_role,
)
# Count is in Content-Range header for HEAD requests
# Fallback to query approach
result = await self.request(
"GET",
f"/rest/v1/{table}",
params={"select": "count", **{k: v for k, v in params.items() if k != "select"}},
headers_override={"Prefer": "count=exact"},
use_service_role=use_service_role,
)
if isinstance(result, list) and len(result) > 0:
return result[0].get("count", 0)
return 0
# =====================
# POSTGRES-META (Admin)
# =====================
async def list_tables(self, schema: str = "public") -> list[dict]:
"""List all tables in a schema."""
return await self.request(
"GET", "/pg/tables", params={"include_system_schemas": "false"}, use_service_role=True
)
async def get_table_schema(self, table: str, schema: str = "public") -> dict:
"""Get table schema/columns."""
columns = await self.request(
"GET", "/pg/columns", params={"table_name": table}, use_service_role=True
)
return {"table": table, "schema": schema, "columns": columns}
async def list_schemas(self) -> list[dict]:
"""List all database schemas."""
return await self.request("GET", "/pg/schemas", use_service_role=True)
async def list_extensions(self) -> list[dict]:
"""List installed extensions."""
return await self.request("GET", "/pg/extensions", use_service_role=True)
async def list_policies(self, table: str | None = None) -> list[dict]:
"""List RLS policies."""
params = {}
if table:
params["table_name"] = table
return await self.request("GET", "/pg/policies", params=params, use_service_role=True)
async def list_roles(self) -> list[dict]:
"""List database roles."""
return await self.request("GET", "/pg/roles", use_service_role=True)
async def list_triggers(self, table: str | None = None) -> list[dict]:
"""List triggers."""
params = {}
if table:
params["table_name"] = table
return await self.request("GET", "/pg/triggers", params=params, use_service_role=True)
async def list_functions(self, schema: str = "public") -> list[dict]:
"""List database functions."""
return await self.request(
"GET", "/pg/functions", params={"schema": schema}, use_service_role=True
)
async def execute_sql(self, query: str) -> Any:
"""Execute raw SQL query."""
return await self.request(
"POST", "/pg/query", json_data={"query": query}, use_service_role=True
)
# =====================
# GOTRUE (Auth)
# =====================
async def list_users(self, page: int = 1, per_page: int = 50) -> dict[str, Any]:
"""List all users (admin)."""
return await self.request(
"GET",
"/auth/v1/admin/users",
params={"page": page, "per_page": per_page},
use_service_role=True,
)
async def get_user(self, user_id: str) -> dict[str, Any]:
"""Get user by ID."""
return await self.request("GET", f"/auth/v1/admin/users/{user_id}", use_service_role=True)
async def create_user(
self,
email: str,
password: str,
email_confirm: bool = False,
phone: str | None = None,
user_metadata: dict | None = None,
app_metadata: dict | None = None,
) -> dict[str, Any]:
"""Create a new user."""
data = {"email": email, "password": password, "email_confirm": email_confirm}
if phone:
data["phone"] = phone
if user_metadata:
data["user_metadata"] = user_metadata
if app_metadata:
data["app_metadata"] = app_metadata
return await self.request(
"POST", "/auth/v1/admin/users", json_data=data, use_service_role=True
)
async def update_user(
self,
user_id: str,
email: str | None = None,
password: str | None = None,
phone: str | None = None,
email_confirm: bool | None = None,
phone_confirm: bool | None = None,
user_metadata: dict | None = None,
app_metadata: dict | None = None,
ban_duration: str | None = None,
) -> dict[str, Any]:
"""Update user."""
data = {}
if email:
data["email"] = email
if password:
data["password"] = password
if phone:
data["phone"] = phone
if email_confirm is not None:
data["email_confirm"] = email_confirm
if phone_confirm is not None:
data["phone_confirm"] = phone_confirm
if user_metadata:
data["user_metadata"] = user_metadata
if app_metadata:
data["app_metadata"] = app_metadata
if ban_duration:
data["ban_duration"] = ban_duration
return await self.request(
"PUT", f"/auth/v1/admin/users/{user_id}", json_data=data, use_service_role=True
)
async def delete_user(self, user_id: str) -> dict[str, Any]:
"""Delete a user."""
return await self.request(
"DELETE", f"/auth/v1/admin/users/{user_id}", use_service_role=True
)
async def generate_link(
self, email: str, link_type: str = "magiclink", redirect_to: str | None = None
) -> dict[str, Any]:
"""Generate magic link or recovery link."""
data = {"email": email, "type": link_type}
if redirect_to:
data["redirect_to"] = redirect_to
return await self.request(
"POST", "/auth/v1/admin/generate_link", json_data=data, use_service_role=True
)
async def list_user_factors(self, user_id: str) -> list[dict]:
"""List user MFA factors."""
return await self.request(
"GET", f"/auth/v1/admin/users/{user_id}/factors", use_service_role=True
)
async def delete_user_factor(self, user_id: str, factor_id: str) -> dict:
"""Delete a user's MFA factor."""
return await self.request(
"DELETE", f"/auth/v1/admin/users/{user_id}/factors/{factor_id}", use_service_role=True
)
# =====================
# STORAGE
# =====================
async def list_buckets(self) -> list[dict]:
"""List all storage buckets."""
return await self.request("GET", "/storage/v1/bucket", use_service_role=True)
async def get_bucket(self, bucket_id: str) -> dict:
"""Get bucket details."""
return await self.request("GET", f"/storage/v1/bucket/{bucket_id}", use_service_role=True)
async def create_bucket(
self,
name: str,
public: bool = False,
file_size_limit: int | None = None,
allowed_mime_types: list[str] | None = None,
) -> dict:
"""Create a new bucket."""
data = {"name": name, "public": public}
if file_size_limit:
data["file_size_limit"] = file_size_limit
if allowed_mime_types:
data["allowed_mime_types"] = allowed_mime_types
return await self.request(
"POST", "/storage/v1/bucket", json_data=data, use_service_role=True
)
async def update_bucket(
self,
bucket_id: str,
public: bool | None = None,
file_size_limit: int | None = None,
allowed_mime_types: list[str] | None = None,
) -> dict:
"""Update bucket settings."""
data = {}
if public is not None:
data["public"] = public
if file_size_limit:
data["file_size_limit"] = file_size_limit
if allowed_mime_types:
data["allowed_mime_types"] = allowed_mime_types
return await self.request(
"PUT", f"/storage/v1/bucket/{bucket_id}", json_data=data, use_service_role=True
)
async def delete_bucket(self, bucket_id: str) -> dict:
"""Delete a bucket."""
return await self.request(
"DELETE", f"/storage/v1/bucket/{bucket_id}", use_service_role=True
)
async def empty_bucket(self, bucket_id: str) -> dict:
"""Empty a bucket (delete all files)."""
return await self.request(
"POST", f"/storage/v1/bucket/{bucket_id}/empty", use_service_role=True
)
async def list_files(
self, bucket: str, path: str = "", limit: int = 100, offset: int = 0
) -> list[dict]:
"""List files in a bucket/path."""
data = {"prefix": path, "limit": limit, "offset": offset}
return await self.request(
"POST", f"/storage/v1/object/list/{bucket}", json_data=data, use_service_role=True
)
async def upload_file(
self,
bucket: str,
path: str,
content: bytes,
content_type: str = "application/octet-stream",
upsert: bool = False,
) -> dict:
"""Upload a file."""
headers = {"Content-Type": content_type}
if upsert:
headers["x-upsert"] = "true"
return await self.request(
"POST",
f"/storage/v1/object/{bucket}/{path}",
data=content,
headers_override=headers,
use_service_role=True,
)
async def download_file(self, bucket: str, path: str) -> dict:
"""Download a file (returns base64)."""
return await self.request(
"GET", f"/storage/v1/object/{bucket}/{path}", use_service_role=True
)
async def delete_files(self, bucket: str, paths: list[str]) -> dict:
"""Delete files from bucket."""
return await self.request(
"DELETE",
f"/storage/v1/object/{bucket}",
json_data={"prefixes": paths},
use_service_role=True,
)
async def move_file(self, bucket: str, from_path: str, to_path: str) -> dict:
"""Move/rename a file."""
return await self.request(
"POST",
"/storage/v1/object/move",
json_data={"bucketId": bucket, "sourceKey": from_path, "destinationKey": to_path},
use_service_role=True,
)
async def get_public_url(self, bucket: str, path: str) -> str:
"""Get public URL for a file."""
return f"{self.base_url}/storage/v1/object/public/{bucket}/{path}"
# =====================
# EDGE FUNCTIONS
# =====================
async def invoke_function(
self, function_name: str, body: dict | None = None, method: str = "POST"
) -> Any:
"""Invoke an Edge Function."""
return await self.request(
method,
f"/functions/v1/{function_name}",
json_data=body,
use_service_role=False, # Use anon key for functions
)
# =====================
# HEALTH CHECK
# =====================
async def health_check(self) -> dict[str, Any]:
"""Check Supabase instance health."""
results = {"healthy": True, "services": {}}
# Check PostgREST
try:
await self.request("GET", "/rest/v1/", use_service_role=True)
results["services"]["postgrest"] = "ok"
except Exception as e:
results["services"]["postgrest"] = f"error: {str(e)}"
results["healthy"] = False
# Check GoTrue
try:
await self.request("GET", "/auth/v1/health", use_service_role=False)
results["services"]["gotrue"] = "ok"
except Exception as e:
results["services"]["gotrue"] = f"error: {str(e)}"
results["healthy"] = False
# Check Storage
try:
await self.list_buckets()
results["services"]["storage"] = "ok"
except Exception as e:
results["services"]["storage"] = f"error: {str(e)}"
results["healthy"] = False
return results

View File

@@ -0,0 +1,23 @@
"""Supabase Plugin Handlers"""
from plugins.supabase.handlers import (
admin,
# Phase G.2
auth,
database,
# Phase G.3
functions,
storage,
system,
)
__all__ = [
"database",
"system",
# Phase G.2
"auth",
"storage",
# Phase G.3
"functions",
"admin",
]

View File

@@ -0,0 +1,625 @@
"""Admin Handler - manages Supabase database administration via postgres-meta"""
import json
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 [
# Extension Management
{
"name": "enable_extension",
"method_name": "enable_extension",
"description": "Enable a PostgreSQL extension (e.g., pgvector, postgis, uuid-ossp).",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Extension name to enable"},
"schema": {
"type": "string",
"description": "Schema to install extension in",
"default": "extensions",
},
},
"required": ["name"],
},
"scope": "admin",
},
{
"name": "disable_extension",
"method_name": "disable_extension",
"description": "Disable (drop) a PostgreSQL extension.",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Extension name to disable"},
"cascade": {
"type": "boolean",
"description": "Drop dependent objects",
"default": False,
},
},
"required": ["name"],
},
"scope": "admin",
},
# RLS Policy Management
{
"name": "create_policy",
"method_name": "create_policy",
"description": "Create a Row Level Security (RLS) policy on a table.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"name": {"type": "string", "description": "Policy name"},
"definition": {
"type": "string",
"description": "USING clause expression (e.g., 'auth.uid() = user_id')",
},
"check": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "WITH CHECK clause expression (for INSERT/UPDATE)",
},
"command": {
"type": "string",
"enum": ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"],
"description": "Command this policy applies to",
"default": "ALL",
},
"roles": {
"type": "array",
"items": {"type": "string"},
"description": "Roles this policy applies to",
"default": ["authenticated"],
},
"schema": {"type": "string", "default": "public"},
},
"required": ["table", "name", "definition"],
},
"scope": "admin",
},
{
"name": "update_policy",
"method_name": "update_policy",
"description": "Update an existing RLS policy. Drops and recreates the policy.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"name": {"type": "string", "description": "Policy name to update"},
"definition": {"type": "string", "description": "New USING clause expression"},
"check": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New WITH CHECK clause expression",
},
"command": {
"type": "string",
"enum": ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"],
"default": "ALL",
},
"roles": {
"type": "array",
"items": {"type": "string"},
"default": ["authenticated"],
},
"schema": {"type": "string", "default": "public"},
},
"required": ["table", "name", "definition"],
},
"scope": "admin",
},
{
"name": "delete_policy",
"method_name": "delete_policy",
"description": "Delete an RLS policy from a table.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"name": {"type": "string", "description": "Policy name to delete"},
"schema": {"type": "string", "default": "public"},
},
"required": ["table", "name"],
},
"scope": "admin",
},
# RLS Table Management
{
"name": "enable_rls",
"method_name": "enable_rls",
"description": "Enable Row Level Security on a table.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"schema": {"type": "string", "default": "public"},
},
"required": ["table"],
},
"scope": "admin",
},
{
"name": "disable_rls",
"method_name": "disable_rls",
"description": "Disable Row Level Security on a table. Warning: This removes all RLS protection.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"schema": {"type": "string", "default": "public"},
},
"required": ["table"],
},
"scope": "admin",
},
# Table Management
{
"name": "create_table",
"method_name": "create_table",
"description": "Create a new database table with specified columns.",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Table name"},
"columns": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"type": {"type": "string"},
"nullable": {"type": "boolean", "default": True},
"default": {"type": "string"},
"primary_key": {"type": "boolean", "default": False},
},
"required": ["name", "type"],
},
"description": "Column definitions",
},
"schema": {"type": "string", "default": "public"},
"enable_rls": {
"type": "boolean",
"description": "Enable RLS on the new table",
"default": True,
},
},
"required": ["name", "columns"],
},
"scope": "admin",
},
{
"name": "drop_table",
"method_name": "drop_table",
"description": "Drop (delete) a database table. Warning: This is irreversible.",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Table name to drop"},
"schema": {"type": "string", "default": "public"},
"cascade": {
"type": "boolean",
"description": "Drop dependent objects",
"default": False,
},
},
"required": ["name"],
},
"scope": "admin",
},
# Column Management
{
"name": "add_column",
"method_name": "add_column",
"description": "Add a new column to an existing table.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"column_name": {"type": "string", "description": "New column name"},
"column_type": {
"type": "string",
"description": "PostgreSQL data type (e.g., 'text', 'integer', 'uuid')",
},
"nullable": {"type": "boolean", "default": True},
"default_value": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Default value expression",
},
"schema": {"type": "string", "default": "public"},
},
"required": ["table", "column_name", "column_type"],
},
"scope": "admin",
},
{
"name": "drop_column",
"method_name": "drop_column",
"description": "Remove a column from a table.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"column_name": {"type": "string", "description": "Column name to drop"},
"schema": {"type": "string", "default": "public"},
"cascade": {"type": "boolean", "default": False},
},
"required": ["table", "column_name"],
},
"scope": "admin",
},
# Database Stats
{
"name": "get_database_size",
"method_name": "get_database_size",
"description": "Get the size of the database and tables.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
]
# =====================
# Admin Operations (12 tools)
# =====================
async def enable_extension(client: SupabaseClient, name: str, schema: str = "extensions") -> str:
"""Enable a PostgreSQL extension"""
try:
query = f'CREATE EXTENSION IF NOT EXISTS "{name}" SCHEMA "{schema}";'
result = await client.execute_sql(query)
return json.dumps(
{
"success": True,
"message": f"Extension '{name}' enabled in schema '{schema}'",
"result": result,
},
indent=2,
ensure_ascii=False,
)
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:
cascade_sql = "CASCADE" if cascade else ""
query = f'DROP EXTENSION IF EXISTS "{name}" {cascade_sql};'
result = await client.execute_sql(query)
return json.dumps(
{
"success": True,
"message": f"Extension '{name}' disabled",
"cascade": cascade,
"result": result,
},
indent=2,
ensure_ascii=False,
)
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,
name: str,
definition: str,
check: str | None = None,
command: str = "ALL",
roles: list[str] | None = None,
schema: str = "public",
) -> str:
"""Create an RLS policy"""
try:
if roles is None:
roles = ["authenticated"]
roles_sql = ", ".join(roles)
check_sql = f"WITH CHECK ({check})" if check else ""
query = f"""
CREATE POLICY "{name}" ON "{schema}"."{table}"
FOR {command}
TO {roles_sql}
USING ({definition})
{check_sql};
"""
result = await client.execute_sql(query)
return json.dumps(
{
"success": True,
"message": f"Policy '{name}' created on table '{table}'",
"table": f"{schema}.{table}",
"policy": {
"name": name,
"command": command,
"roles": roles,
"using": definition,
"check": check,
},
"result": result,
},
indent=2,
ensure_ascii=False,
)
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,
name: str,
definition: str,
check: str | None = None,
command: str = "ALL",
roles: list[str] | None = None,
schema: str = "public",
) -> str:
"""Update an RLS policy by dropping and recreating"""
try:
# First drop the existing policy
drop_query = f'DROP POLICY IF EXISTS "{name}" ON "{schema}"."{table}";'
await client.execute_sql(drop_query)
# Then create the new one
return await create_policy(
client=client,
table=table,
name=name,
definition=definition,
check=check,
command=command,
roles=roles,
schema=schema,
)
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:
"""Delete an RLS policy"""
try:
query = f'DROP POLICY IF EXISTS "{name}" ON "{schema}"."{table}";'
result = await client.execute_sql(query)
return json.dumps(
{
"success": True,
"message": f"Policy '{name}' deleted from table '{table}'",
"result": result,
},
indent=2,
ensure_ascii=False,
)
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:
query = f'ALTER TABLE "{schema}"."{table}" ENABLE ROW LEVEL SECURITY;'
result = await client.execute_sql(query)
return json.dumps(
{
"success": True,
"message": f"RLS enabled on table '{schema}.{table}'",
"result": result,
},
indent=2,
ensure_ascii=False,
)
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:
query = f'ALTER TABLE "{schema}"."{table}" DISABLE ROW LEVEL SECURITY;'
result = await client.execute_sql(query)
return json.dumps(
{
"success": True,
"message": f"RLS disabled on table '{schema}.{table}'",
"warning": "Table is now accessible without RLS protection",
"result": result,
},
indent=2,
ensure_ascii=False,
)
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,
columns: list[dict],
schema: str = "public",
enable_rls: bool = True,
) -> str:
"""Create a new database table"""
try:
# Build column definitions
col_defs = []
for col in columns:
col_def = f'"{col["name"]}" {col["type"]}'
if col.get("primary_key"):
col_def += " PRIMARY KEY"
if not col.get("nullable", True):
col_def += " NOT NULL"
if "default" in col:
col_def += f' DEFAULT {col["default"]}'
col_defs.append(col_def)
columns_sql = ",\n ".join(col_defs)
query = f"""
CREATE TABLE "{schema}"."{name}" (
{columns_sql}
);
"""
result = await client.execute_sql(query)
# Enable RLS if requested
if enable_rls:
rls_query = f'ALTER TABLE "{schema}"."{name}" ENABLE ROW LEVEL SECURITY;'
await client.execute_sql(rls_query)
return json.dumps(
{
"success": True,
"message": f"Table '{schema}.{name}' created",
"columns": columns,
"rls_enabled": enable_rls,
"result": result,
},
indent=2,
ensure_ascii=False,
)
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:
"""Drop a database table"""
try:
cascade_sql = "CASCADE" if cascade else ""
query = f'DROP TABLE IF EXISTS "{schema}"."{name}" {cascade_sql};'
result = await client.execute_sql(query)
return json.dumps(
{
"success": True,
"message": f"Table '{schema}.{name}' dropped",
"cascade": cascade,
"result": result,
},
indent=2,
ensure_ascii=False,
)
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,
column_name: str,
column_type: str,
nullable: bool = True,
default_value: str | None = None,
schema: str = "public",
) -> str:
"""Add a column to a table"""
try:
nullable_sql = "" if nullable else "NOT NULL"
default_sql = f"DEFAULT {default_value}" if default_value else ""
query = f"""
ALTER TABLE "{schema}"."{table}"
ADD COLUMN "{column_name}" {column_type} {nullable_sql} {default_sql};
"""
result = await client.execute_sql(query)
return json.dumps(
{
"success": True,
"message": f"Column '{column_name}' added to table '{schema}.{table}'",
"column": {
"name": column_name,
"type": column_type,
"nullable": nullable,
"default": default_value,
},
"result": result,
},
indent=2,
ensure_ascii=False,
)
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,
column_name: str,
schema: str = "public",
cascade: bool = False,
) -> str:
"""Drop a column from a table"""
try:
cascade_sql = "CASCADE" if cascade else ""
query = f"""
ALTER TABLE "{schema}"."{table}"
DROP COLUMN "{column_name}" {cascade_sql};
"""
result = await client.execute_sql(query)
return json.dumps(
{
"success": True,
"message": f"Column '{column_name}' dropped from table '{schema}.{table}'",
"cascade": cascade,
"result": result,
},
indent=2,
ensure_ascii=False,
)
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:
# Get database size
db_query = """
SELECT
pg_database.datname AS database_name,
pg_size_pretty(pg_database_size(pg_database.datname)) AS size
FROM pg_database
WHERE pg_database.datname = current_database();
"""
db_result = await client.execute_sql(db_query)
# Get table sizes
tables_query = """
SELECT
schemaname AS schema,
tablename AS table,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS total_size,
pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) AS data_size,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename) - pg_relation_size(schemaname || '.' || tablename)) AS index_size
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC
LIMIT 20;
"""
tables_result = await client.execute_sql(tables_query)
return json.dumps(
{
"success": True,
"database": db_result[0] if db_result else {},
"top_tables": tables_result,
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)

View File

@@ -0,0 +1,540 @@
"""Auth Handler - manages Supabase authentication via GoTrue Admin API"""
import json
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 [
{
"name": "list_users",
"method_name": "list_users",
"description": "List all users with pagination. Returns user details including email, phone, metadata, and confirmation status.",
"schema": {
"type": "object",
"properties": {
"page": {
"type": "integer",
"description": "Page number (1-indexed)",
"default": 1,
},
"per_page": {
"type": "integer",
"description": "Users per page (max 1000)",
"default": 50,
},
},
},
"scope": "admin",
},
{
"name": "get_user",
"method_name": "get_user",
"description": "Get detailed information about a specific user by their ID.",
"schema": {
"type": "object",
"properties": {"user_id": {"type": "string", "description": "User UUID"}},
"required": ["user_id"],
},
"scope": "read",
},
{
"name": "create_user",
"method_name": "create_user",
"description": "Create a new user with email and password. Can optionally auto-confirm email and set metadata.",
"schema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"description": "User email address",
},
"password": {
"type": "string",
"minLength": 6,
"description": "Password (min 6 characters)",
},
"phone": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Phone number in E.164 format",
},
"email_confirm": {
"type": "boolean",
"description": "Auto-confirm email without verification",
"default": False,
},
"user_metadata": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Custom user metadata (name, avatar, etc.)",
},
"app_metadata": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "App-specific metadata (roles, permissions)",
},
},
"required": ["email", "password"],
},
"scope": "admin",
},
{
"name": "update_user",
"method_name": "update_user",
"description": "Update user details including email, password, phone, metadata, or ban status.",
"schema": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "User UUID"},
"email": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New email address",
},
"password": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New password",
},
"phone": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New phone number",
},
"email_confirm": {
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"description": "Set email confirmation status",
},
"phone_confirm": {
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"description": "Set phone confirmation status",
},
"user_metadata": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Update user metadata",
},
"app_metadata": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Update app metadata",
},
},
"required": ["user_id"],
},
"scope": "admin",
},
{
"name": "delete_user",
"method_name": "delete_user",
"description": "Permanently delete a user and all their data.",
"schema": {
"type": "object",
"properties": {"user_id": {"type": "string", "description": "User UUID to delete"}},
"required": ["user_id"],
},
"scope": "admin",
},
{
"name": "invite_user",
"method_name": "invite_user",
"description": "Send an email invitation to a new user. Creates user in 'invited' state.",
"schema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"description": "Email to invite",
},
"redirect_to": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "URL to redirect after accepting invitation",
},
"user_metadata": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Initial user metadata",
},
},
"required": ["email"],
},
"scope": "admin",
},
{
"name": "generate_link",
"method_name": "generate_link",
"description": "Generate a magic link, recovery link, or invite link for a user.",
"schema": {
"type": "object",
"properties": {
"email": {"type": "string", "format": "email", "description": "User email"},
"link_type": {
"type": "string",
"enum": [
"magiclink",
"recovery",
"invite",
"signup",
"email_change_new",
"email_change_current",
],
"description": "Type of link to generate",
"default": "magiclink",
},
"redirect_to": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "URL to redirect after link is used",
},
},
"required": ["email"],
},
"scope": "admin",
},
{
"name": "ban_user",
"method_name": "ban_user",
"description": "Ban a user for a specified duration. Banned users cannot sign in.",
"schema": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "User UUID"},
"duration": {
"type": "string",
"description": "Ban duration (e.g., '24h', '7d', '1y', 'none' for permanent)",
"default": "none",
},
},
"required": ["user_id"],
},
"scope": "admin",
},
{
"name": "unban_user",
"method_name": "unban_user",
"description": "Remove ban from a user, allowing them to sign in again.",
"schema": {
"type": "object",
"properties": {"user_id": {"type": "string", "description": "User UUID"}},
"required": ["user_id"],
},
"scope": "admin",
},
{
"name": "list_user_factors",
"method_name": "list_user_factors",
"description": "List all MFA factors (TOTP, phone) for a user.",
"schema": {
"type": "object",
"properties": {"user_id": {"type": "string", "description": "User UUID"}},
"required": ["user_id"],
},
"scope": "read",
},
{
"name": "delete_user_factor",
"method_name": "delete_user_factor",
"description": "Delete an MFA factor from a user.",
"schema": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "User UUID"},
"factor_id": {"type": "string", "description": "Factor UUID to delete"},
},
"required": ["user_id", "factor_id"],
},
"scope": "admin",
},
{
"name": "get_auth_config",
"method_name": "get_auth_config",
"description": "Get current GoTrue authentication configuration.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "search_users",
"method_name": "search_users",
"description": "Search users by email or phone number.",
"schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query (email or phone)"},
"page": {"type": "integer", "default": 1},
"per_page": {"type": "integer", "default": 50},
},
"required": ["query"],
},
"scope": "read",
},
{
"name": "get_user_by_email",
"method_name": "get_user_by_email",
"description": "Find a user by their email address.",
"schema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"description": "Email to search for",
}
},
"required": ["email"],
},
"scope": "read",
},
]
# =====================
# Auth Operations (14 tools)
# =====================
async def list_users(client: SupabaseClient, page: int = 1, per_page: int = 50) -> str:
"""List all users with pagination"""
try:
result = await client.list_users(page=page, per_page=per_page)
users = result.get("users", []) if isinstance(result, dict) else result
result.get("aud", len(users)) if isinstance(result, dict) else len(users)
return json.dumps(
{
"success": True,
"page": page,
"per_page": per_page,
"count": len(users) if isinstance(users, list) else 0,
"users": users,
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.get_user(user_id)
return json.dumps({"success": True, "user": result}, indent=2, ensure_ascii=False)
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,
password: str,
phone: str | None = None,
email_confirm: bool = False,
user_metadata: dict | None = None,
app_metadata: dict | None = None,
) -> str:
"""Create a new user"""
try:
result = await client.create_user(
email=email,
password=password,
phone=phone,
email_confirm=email_confirm,
user_metadata=user_metadata,
app_metadata=app_metadata,
)
return json.dumps(
{"success": True, "message": "User created successfully", "user": result},
indent=2,
ensure_ascii=False,
)
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,
email: str | None = None,
password: str | None = None,
phone: str | None = None,
email_confirm: bool | None = None,
phone_confirm: bool | None = None,
user_metadata: dict | None = None,
app_metadata: dict | None = None,
) -> str:
"""Update user details"""
try:
result = await client.update_user(
user_id=user_id,
email=email,
password=password,
phone=phone,
email_confirm=email_confirm,
phone_confirm=phone_confirm,
user_metadata=user_metadata,
app_metadata=app_metadata,
)
return json.dumps(
{"success": True, "message": "User updated successfully", "user": result},
indent=2,
ensure_ascii=False,
)
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:
await client.delete_user(user_id)
return json.dumps(
{"success": True, "message": f"User {user_id} deleted successfully"},
indent=2,
ensure_ascii=False,
)
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,
redirect_to: str | None = None,
user_metadata: dict | None = None,
) -> str:
"""Invite a user by email"""
try:
result = await client.generate_link(
email=email, link_type="invite", redirect_to=redirect_to
)
return json.dumps(
{"success": True, "message": f"Invitation sent to {email}", "result": result},
indent=2,
ensure_ascii=False,
)
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:
"""Generate auth link (magic link, recovery, invite)"""
try:
result = await client.generate_link(
email=email, link_type=link_type, redirect_to=redirect_to
)
return json.dumps(
{"success": True, "link_type": link_type, "email": email, "result": result},
indent=2,
ensure_ascii=False,
)
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:
result = await client.update_user(user_id=user_id, ban_duration=duration)
return json.dumps(
{"success": True, "message": f"User {user_id} banned for {duration}", "user": result},
indent=2,
ensure_ascii=False,
)
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:
result = await client.update_user(user_id=user_id, ban_duration="0")
return json.dumps(
{"success": True, "message": f"User {user_id} unbanned", "user": result},
indent=2,
ensure_ascii=False,
)
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:
result = await client.list_user_factors(user_id)
return json.dumps(
{"success": True, "user_id": user_id, "factors": result}, indent=2, ensure_ascii=False
)
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:
await client.delete_user_factor(user_id, factor_id)
return json.dumps(
{"success": True, "message": f"Factor {factor_id} deleted from user {user_id}"},
indent=2,
ensure_ascii=False,
)
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:
# Get health which includes some config info
result = await client.request("GET", "/auth/v1/health", use_service_role=False)
return json.dumps({"success": True, "config": result}, indent=2, ensure_ascii=False)
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:
"""Search users by email or phone"""
try:
# Get all users and filter
result = await client.list_users(page=page, per_page=per_page)
users = result.get("users", []) if isinstance(result, dict) else result
# Filter by query
query_lower = query.lower()
filtered = [
u
for u in users
if (
u.get("email", "").lower().find(query_lower) >= 0
or u.get("phone", "").find(query) >= 0
)
]
return json.dumps(
{"success": True, "query": query, "count": len(filtered), "users": filtered},
indent=2,
ensure_ascii=False,
)
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:
# Get all users and find by email
result = await client.list_users(page=1, per_page=1000)
users = result.get("users", []) if isinstance(result, dict) else result
# Find exact match
user = next((u for u in users if u.get("email", "").lower() == email.lower()), None)
if user:
return json.dumps(
{"success": True, "found": True, "user": user}, indent=2, ensure_ascii=False
)
else:
return json.dumps(
{"success": True, "found": False, "message": f"No user found with email: {email}"},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)

View File

@@ -0,0 +1,940 @@
"""Database Handler - manages Supabase database operations via PostgREST and postgres-meta"""
import json
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 [
# =====================
# PostgREST Operations (6)
# =====================
{
"name": "query_table",
"method_name": "query_table",
"description": "Query data from a table with filters, sorting, and pagination. Uses PostgREST operators: eq, neq, gt, gte, lt, lte, like, ilike, in, is, cs (contains), cd (contained).",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name to query"},
"select": {
"type": "string",
"description": "Columns to select (default: *). Use * for all, or comma-separated list",
"default": "*",
},
"filters": {
"anyOf": [
{
"type": "array",
"items": {
"type": "object",
"properties": {
"column": {"type": "string"},
"operator": {
"type": "string",
"enum": [
"eq",
"neq",
"gt",
"gte",
"lt",
"lte",
"like",
"ilike",
"in",
"is",
"cs",
"cd",
],
},
"value": {},
},
"required": ["column", "value"],
},
},
{"type": "null"},
],
"description": "Filter conditions. Each filter has column, operator (default: eq), and value",
},
"order": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Order by clause (e.g., 'created_at.desc' or 'name.asc')",
},
"limit": {
"type": "integer",
"description": "Maximum rows to return",
"default": 100,
},
"offset": {
"type": "integer",
"description": "Number of rows to skip for pagination",
"default": 0,
},
"use_service_role": {
"type": "boolean",
"description": "Use service_role key to bypass RLS policies",
"default": False,
},
},
"required": ["table"],
},
"scope": "read",
},
{
"name": "insert_rows",
"method_name": "insert_rows",
"description": "Insert one or more rows into a table. Supports upsert mode for insert-or-update operations.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"rows": {
"type": "array",
"items": {"type": "object"},
"description": "Array of row objects to insert",
},
"upsert": {
"type": "boolean",
"description": "Enable upsert mode (insert or update on conflict)",
"default": False,
},
"on_conflict": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Column(s) to check for conflict (for upsert)",
},
"use_service_role": {
"type": "boolean",
"description": "Use service_role key to bypass RLS policies",
"default": False,
},
},
"required": ["table", "rows"],
},
"scope": "write",
},
{
"name": "update_rows",
"method_name": "update_rows",
"description": "Update rows matching filter conditions. Always requires at least one filter to prevent accidental full-table updates.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"data": {"type": "object", "description": "Fields to update with new values"},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"column": {"type": "string"},
"operator": {
"type": "string",
"enum": [
"eq",
"neq",
"gt",
"gte",
"lt",
"lte",
"like",
"ilike",
"in",
"is",
],
},
"value": {},
},
"required": ["column", "value"],
},
"description": "Filter conditions to match rows for update",
"minItems": 1,
},
"use_service_role": {
"type": "boolean",
"description": "Use service_role key to bypass RLS policies",
"default": False,
},
},
"required": ["table", "data", "filters"],
},
"scope": "write",
},
{
"name": "delete_rows",
"method_name": "delete_rows",
"description": "Delete rows matching filter conditions. Always requires at least one filter to prevent accidental full-table deletion.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"column": {"type": "string"},
"operator": {
"type": "string",
"enum": [
"eq",
"neq",
"gt",
"gte",
"lt",
"lte",
"like",
"ilike",
"in",
"is",
],
},
"value": {},
},
"required": ["column", "value"],
},
"description": "Filter conditions to match rows for deletion",
"minItems": 1,
},
"use_service_role": {
"type": "boolean",
"description": "Use service_role key to bypass RLS policies",
"default": False,
},
},
"required": ["table", "filters"],
},
"scope": "write",
},
{
"name": "execute_rpc",
"method_name": "execute_rpc",
"description": "Execute a stored procedure or database function via RPC. Functions must be exposed through PostgREST.",
"schema": {
"type": "object",
"properties": {
"function_name": {
"type": "string",
"description": "Name of the stored procedure/function",
},
"params": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Parameters to pass to the function",
},
"use_service_role": {
"type": "boolean",
"description": "Use service_role key to bypass RLS policies",
"default": False,
},
},
"required": ["function_name"],
},
"scope": "write",
},
{
"name": "count_rows",
"method_name": "count_rows",
"description": "Count rows in a table, optionally matching filter conditions.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"column": {"type": "string"},
"operator": {"type": "string"},
"value": {},
},
"required": ["column", "value"],
},
"description": "Optional filter conditions (omit for full count)",
"default": [],
},
"use_service_role": {
"type": "boolean",
"description": "Use service_role key to bypass RLS policies",
"default": False,
},
},
"required": ["table"],
},
"scope": "read",
},
# =====================
# Admin Operations via postgres-meta (12)
# =====================
{
"name": "list_tables",
"method_name": "list_tables",
"description": "List all tables in the database with pagination. Returns table names, schemas, and basic metadata.",
"schema": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Schema to list tables from",
"default": "public",
},
"limit": {
"type": "integer",
"description": "Maximum tables to return (default: 50)",
"default": 50,
},
"offset": {
"type": "integer",
"description": "Number of tables to skip for pagination",
"default": 0,
},
},
},
"scope": "read",
},
{
"name": "get_table_schema",
"method_name": "get_table_schema",
"description": "Get detailed schema information for a table including all columns, data types, constraints, and defaults.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"schema": {"type": "string", "description": "Schema name", "default": "public"},
},
"required": ["table"],
},
"scope": "read",
},
{
"name": "list_schemas",
"method_name": "list_schemas",
"description": "List all database schemas (namespaces).",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "list_extensions",
"method_name": "list_extensions",
"description": "List installed PostgreSQL extensions (e.g., pgvector, postgis, uuid-ossp).",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "list_policies",
"method_name": "list_policies",
"description": "List Row Level Security (RLS) policies. Can filter by table name.",
"schema": {
"type": "object",
"properties": {
"table": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Filter policies for specific table",
}
},
},
"scope": "read",
},
{
"name": "list_roles",
"method_name": "list_roles",
"description": "List all database roles with their attributes and permissions.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "list_triggers",
"method_name": "list_triggers",
"description": "List database triggers. Can filter by table name.",
"schema": {
"type": "object",
"properties": {
"table": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Filter triggers for specific table",
}
},
},
"scope": "read",
},
{
"name": "list_functions",
"method_name": "list_functions",
"description": "List database functions/stored procedures in a schema with pagination.",
"schema": {
"type": "object",
"properties": {
"schema": {
"type": "string",
"description": "Schema to list functions from",
"default": "public",
},
"limit": {
"type": "integer",
"description": "Maximum functions to return (default: 50)",
"default": 50,
},
"offset": {
"type": "integer",
"description": "Number of functions to skip for pagination",
"default": 0,
},
},
},
"scope": "read",
},
{
"name": "execute_sql",
"method_name": "execute_sql",
"description": "Execute raw SQL query. Use with caution - bypasses RLS. Returns query results.",
"schema": {
"type": "object",
"properties": {"query": {"type": "string", "description": "SQL query to execute"}},
"required": ["query"],
},
"scope": "admin",
},
{
"name": "get_table_indexes",
"method_name": "get_table_indexes",
"description": "Get indexes for a table including primary keys, unique constraints, and custom indexes.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"schema": {"type": "string", "description": "Schema name", "default": "public"},
},
"required": ["table"],
},
"scope": "read",
},
{
"name": "get_table_constraints",
"method_name": "get_table_constraints",
"description": "Get all constraints (primary key, foreign key, unique, check) for a table.",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"schema": {"type": "string", "description": "Schema name", "default": "public"},
},
"required": ["table"],
},
"scope": "read",
},
{
"name": "get_table_relationships",
"method_name": "get_table_relationships",
"description": "Get foreign key relationships for a table (both references to and from this table).",
"schema": {
"type": "object",
"properties": {
"table": {"type": "string", "description": "Table name"},
"schema": {"type": "string", "description": "Schema name", "default": "public"},
},
"required": ["table"],
},
"scope": "read",
},
]
# =====================
# PostgREST Operations (6)
# =====================
async def query_table(
client: SupabaseClient,
table: str,
select: str = "*",
filters: list[dict] | None = None,
order: str | None = None,
limit: int = 100,
offset: int = 0,
use_service_role: bool = False,
) -> str:
"""Query data from a table"""
try:
result = await client.query_table(
table=table,
select=select,
filters=filters,
order=order,
limit=limit,
offset=offset,
use_service_role=use_service_role,
)
return json.dumps(
{
"success": True,
"table": table,
"count": len(result) if isinstance(result, list) else 1,
"data": result,
},
indent=2,
ensure_ascii=False,
)
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,
rows: list[dict],
upsert: bool = False,
on_conflict: str | None = None,
use_service_role: bool = False,
) -> str:
"""Insert rows into a table"""
try:
result = await client.insert_rows(
table=table,
rows=rows,
upsert=upsert,
on_conflict=on_conflict,
use_service_role=use_service_role,
)
return json.dumps(
{
"success": True,
"table": table,
"inserted": len(result) if isinstance(result, list) else 1,
"data": result,
},
indent=2,
ensure_ascii=False,
)
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,
data: dict,
filters: list[dict],
use_service_role: bool = False,
) -> str:
"""Update rows in a table"""
try:
if not filters:
return json.dumps(
{
"success": False,
"error": "At least one filter is required to prevent accidental full-table updates",
},
indent=2,
ensure_ascii=False,
)
result = await client.update_rows(
table=table, data=data, filters=filters, use_service_role=use_service_role
)
return json.dumps(
{
"success": True,
"table": table,
"updated": len(result) if isinstance(result, list) else 1,
"data": result,
},
indent=2,
ensure_ascii=False,
)
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:
"""Delete rows from a table"""
try:
if not filters:
return json.dumps(
{
"success": False,
"error": "At least one filter is required to prevent accidental full-table deletion",
},
indent=2,
ensure_ascii=False,
)
result = await client.delete_rows(
table=table, filters=filters, use_service_role=use_service_role
)
return json.dumps(
{
"success": True,
"table": table,
"deleted": len(result) if isinstance(result, list) else 1,
"data": result,
},
indent=2,
ensure_ascii=False,
)
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,
params: dict | None = None,
use_service_role: bool = False,
) -> str:
"""Execute a stored procedure/function"""
try:
result = await client.execute_rpc(
function_name=function_name, params=params, use_service_role=use_service_role
)
return json.dumps(
{"success": True, "function": function_name, "result": result},
indent=2,
ensure_ascii=False,
)
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,
filters: list[dict] | None = None,
use_service_role: bool = False,
) -> str:
"""Count rows in a table"""
try:
count = await client.count_rows(
table=table, filters=filters, use_service_role=use_service_role
)
return json.dumps(
{
"success": True,
"table": table,
"count": count,
"filters_applied": len(filters) if filters else 0,
},
indent=2,
ensure_ascii=False,
)
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:
"""List all tables in the database with pagination"""
try:
result = await client.list_tables(schema=schema)
# Filter by schema if needed
if isinstance(result, list):
tables = [t for t in result if t.get("schema") == schema]
else:
tables = result if result else []
# Apply pagination
total_count = len(tables)
paginated = tables[offset : offset + limit] if isinstance(tables, list) else tables
return json.dumps(
{
"success": True,
"schema": schema,
"total_count": total_count,
"count": len(paginated) if isinstance(paginated, list) else 0,
"limit": limit,
"offset": offset,
"has_more": (offset + limit) < total_count,
"tables": paginated,
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.get_table_schema(table=table, schema=schema)
return json.dumps(
{
"success": True,
"table": table,
"schema": schema,
"columns": result.get("columns", []),
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.list_schemas()
return json.dumps(
{
"success": True,
"count": len(result) if isinstance(result, list) else 0,
"schemas": result,
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.list_extensions()
return json.dumps(
{
"success": True,
"count": len(result) if isinstance(result, list) else 0,
"extensions": result,
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.list_policies(table=table)
return json.dumps(
{
"success": True,
"table_filter": table,
"count": len(result) if isinstance(result, list) else 0,
"policies": result,
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.list_roles()
return json.dumps(
{
"success": True,
"count": len(result) if isinstance(result, list) else 0,
"roles": result,
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.list_triggers(table=table)
return json.dumps(
{
"success": True,
"table_filter": table,
"count": len(result) if isinstance(result, list) else 0,
"triggers": result,
},
indent=2,
ensure_ascii=False,
)
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:
"""List database functions with pagination"""
try:
result = await client.list_functions(schema=schema)
# Apply pagination
functions = result if isinstance(result, list) else []
total_count = len(functions)
paginated = functions[offset : offset + limit]
return json.dumps(
{
"success": True,
"schema": schema,
"total_count": total_count,
"count": len(paginated),
"limit": limit,
"offset": offset,
"has_more": (offset + limit) < total_count,
"functions": paginated,
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.execute_sql(query=query)
return json.dumps(
{
"success": True,
"query": query[:200] + "..." if len(query) > 200 else query,
"result": result,
},
indent=2,
ensure_ascii=False,
)
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:
query = f"""
SELECT
i.relname AS index_name,
a.attname AS column_name,
am.amname AS index_type,
ix.indisunique AS is_unique,
ix.indisprimary AS is_primary
FROM
pg_index ix
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_class t ON t.oid = ix.indrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN pg_am am ON am.oid = i.relam
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
WHERE
t.relname = '{table}'
AND n.nspname = '{schema}'
ORDER BY i.relname;
"""
result = await client.execute_sql(query=query)
return json.dumps(
{"success": True, "table": table, "schema": schema, "indexes": result},
indent=2,
ensure_ascii=False,
)
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:
query = f"""
SELECT
con.conname AS constraint_name,
con.contype AS constraint_type,
CASE con.contype
WHEN 'p' THEN 'PRIMARY KEY'
WHEN 'f' THEN 'FOREIGN KEY'
WHEN 'u' THEN 'UNIQUE'
WHEN 'c' THEN 'CHECK'
WHEN 'x' THEN 'EXCLUSION'
END AS constraint_type_name,
pg_get_constraintdef(con.oid) AS definition
FROM
pg_constraint con
JOIN pg_class rel ON rel.oid = con.conrelid
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
WHERE
rel.relname = '{table}'
AND nsp.nspname = '{schema}'
ORDER BY con.contype;
"""
result = await client.execute_sql(query=query)
return json.dumps(
{"success": True, "table": table, "schema": schema, "constraints": result},
indent=2,
ensure_ascii=False,
)
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:
"""Get foreign key relationships for a table"""
try:
# Get foreign keys from this table
outgoing_query = f"""
SELECT
con.conname AS constraint_name,
att.attname AS column_name,
ref_class.relname AS referenced_table,
ref_att.attname AS referenced_column
FROM
pg_constraint con
JOIN pg_class rel ON rel.oid = con.conrelid
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ANY(con.conkey)
JOIN pg_class ref_class ON ref_class.oid = con.confrelid
JOIN pg_attribute ref_att ON ref_att.attrelid = con.confrelid AND ref_att.attnum = ANY(con.confkey)
WHERE
con.contype = 'f'
AND rel.relname = '{table}'
AND nsp.nspname = '{schema}';
"""
# Get foreign keys referencing this table
incoming_query = f"""
SELECT
con.conname AS constraint_name,
src_class.relname AS source_table,
att.attname AS source_column,
ref_att.attname AS referenced_column
FROM
pg_constraint con
JOIN pg_class src_class ON src_class.oid = con.conrelid
JOIN pg_class ref_class ON ref_class.oid = con.confrelid
JOIN pg_namespace nsp ON nsp.oid = ref_class.relnamespace
JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ANY(con.conkey)
JOIN pg_attribute ref_att ON ref_att.attrelid = con.confrelid AND ref_att.attnum = ANY(con.confkey)
WHERE
con.contype = 'f'
AND ref_class.relname = '{table}'
AND nsp.nspname = '{schema}';
"""
outgoing = await client.execute_sql(query=outgoing_query)
incoming = await client.execute_sql(query=incoming_query)
return json.dumps(
{
"success": True,
"table": table,
"schema": schema,
"outgoing_references": outgoing,
"incoming_references": incoming,
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)

View File

@@ -0,0 +1,383 @@
"""Functions Handler - manages Supabase Edge Functions"""
import json
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 [
{
"name": "invoke_function",
"method_name": "invoke_function",
"description": "Invoke a Supabase Edge Function with POST method. Pass data in the body parameter.",
"schema": {
"type": "object",
"properties": {
"function_name": {
"type": "string",
"description": "Name of the Edge Function to invoke",
},
"body": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "JSON body to send to the function",
},
"headers": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Additional headers to send",
},
},
"required": ["function_name"],
},
"scope": "write",
},
{
"name": "invoke_function_get",
"method_name": "invoke_function_get",
"description": "Invoke a Supabase Edge Function with GET method. Use for read-only operations.",
"schema": {
"type": "object",
"properties": {
"function_name": {
"type": "string",
"description": "Name of the Edge Function to invoke",
},
"params": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Query parameters to send",
},
},
"required": ["function_name"],
},
"scope": "read",
},
{
"name": "list_edge_functions",
"method_name": "list_edge_functions",
"description": "List all deployed Edge Functions. Note: This reads from the functions volume in Self-Hosted.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "get_function_info",
"method_name": "get_function_info",
"description": "Get information about a specific Edge Function including its configuration.",
"schema": {
"type": "object",
"properties": {
"function_name": {"type": "string", "description": "Name of the function"}
},
"required": ["function_name"],
},
"scope": "read",
},
{
"name": "test_function",
"method_name": "test_function",
"description": "Test an Edge Function with sample data and return the response.",
"schema": {
"type": "object",
"properties": {
"function_name": {
"type": "string",
"description": "Name of the function to test",
},
"test_data": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Test data to send",
},
"method": {
"type": "string",
"enum": ["GET", "POST"],
"description": "HTTP method",
"default": "POST",
},
},
"required": ["function_name"],
},
"scope": "write",
},
{
"name": "get_function_url",
"method_name": "get_function_url",
"description": "Get the full URL for invoking an Edge Function.",
"schema": {
"type": "object",
"properties": {
"function_name": {"type": "string", "description": "Name of the function"}
},
"required": ["function_name"],
},
"scope": "read",
},
{
"name": "check_function_health",
"method_name": "check_function_health",
"description": "Check if an Edge Function is responding correctly.",
"schema": {
"type": "object",
"properties": {
"function_name": {"type": "string", "description": "Name of the function"}
},
"required": ["function_name"],
},
"scope": "read",
},
{
"name": "invoke_function_batch",
"method_name": "invoke_function_batch",
"description": "Invoke an Edge Function multiple times with different payloads. Useful for batch processing.",
"schema": {
"type": "object",
"properties": {
"function_name": {"type": "string", "description": "Name of the function"},
"payloads": {
"type": "array",
"items": {"type": "object"},
"description": "Array of payloads to send",
},
},
"required": ["function_name", "payloads"],
},
"scope": "write",
},
]
# =====================
# Functions Operations (8 tools)
# =====================
async def invoke_function(
client: SupabaseClient,
function_name: str,
body: dict | None = None,
headers: dict | None = None,
) -> str:
"""Invoke an Edge Function with POST"""
try:
result = await client.invoke_function(function_name=function_name, body=body, method="POST")
return json.dumps(
{"success": True, "function": function_name, "method": "POST", "response": result},
indent=2,
ensure_ascii=False,
)
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:
"""Invoke an Edge Function with GET"""
try:
# Build query string
endpoint = f"/functions/v1/{function_name}"
if params:
query = "&".join(f"{k}={v}" for k, v in params.items())
endpoint = f"{endpoint}?{query}"
result = await client.request("GET", endpoint, use_service_role=False)
return json.dumps(
{
"success": True,
"function": function_name,
"method": "GET",
"params": params,
"response": result,
},
indent=2,
ensure_ascii=False,
)
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:
# In self-hosted, we can try to list functions via the API
# or check the functions volume. We'll try a health-check approach.
functions_info = {
"note": "In Self-Hosted Supabase, Edge Functions are deployed in volumes/functions/",
"endpoint": f"{client.base_url}/functions/v1/",
"available": True,
}
# Try to verify the functions endpoint is available
try:
await client.request("GET", "/functions/v1/", use_service_role=False)
functions_info["status"] = "Functions endpoint available"
except Exception as e:
functions_info["status"] = f"Functions endpoint check: {str(e)}"
return json.dumps(
{
"success": True,
"functions_info": functions_info,
"tip": "Deploy functions by adding them to volumes/functions/ directory",
},
indent=2,
ensure_ascii=False,
)
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:
info = {
"name": function_name,
"url": f"{client.base_url}/functions/v1/{function_name}",
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"auth_required": True,
"cors_enabled": True,
}
# Try to check if function exists by making a request
try:
await client.request(
"OPTIONS", f"/functions/v1/{function_name}", use_service_role=False
)
info["status"] = "available"
except Exception as e:
if "404" in str(e):
info["status"] = "not_found"
else:
info["status"] = "unknown"
info["check_error"] = str(e)
return json.dumps({"success": True, "function_info": info}, indent=2, ensure_ascii=False)
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:
"""Test an Edge Function"""
try:
if method.upper() == "GET":
result = await client.request(
"GET", f"/functions/v1/{function_name}", params=test_data, use_service_role=False
)
else:
result = await client.invoke_function(
function_name=function_name, body=test_data, method="POST"
)
return json.dumps(
{
"success": True,
"function": function_name,
"method": method.upper(),
"test_data": test_data,
"response": result,
"status": "Function responded successfully",
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps(
{
"success": False,
"function": function_name,
"method": method.upper(),
"test_data": test_data,
"error": str(e),
"status": "Function test failed",
},
indent=2,
ensure_ascii=False,
)
async def get_function_url(client: SupabaseClient, function_name: str) -> str:
"""Get the full URL for a function"""
try:
url = f"{client.base_url}/functions/v1/{function_name}"
return json.dumps(
{
"success": True,
"function": function_name,
"url": url,
"curl_example": f"curl -X POST '{url}' -H 'Authorization: Bearer <anon_key>' -H 'Content-Type: application/json' -d '{{}}'",
},
indent=2,
ensure_ascii=False,
)
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:
healthy = False
response_time = None
error = None
import time
start = time.time()
try:
# Try to invoke with empty body
await client.invoke_function(function_name=function_name, body={}, method="POST")
healthy = True
except Exception as e:
error = str(e)
# 400 errors might mean the function is running but needs valid input
if "400" in str(e) or "422" in str(e):
healthy = True
error = "Function running (returned validation error)"
response_time = round((time.time() - start) * 1000, 2)
return json.dumps(
{
"success": True,
"function": function_name,
"healthy": healthy,
"response_time_ms": response_time,
"error": error,
},
indent=2,
ensure_ascii=False,
)
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:
"""Invoke a function with multiple payloads"""
try:
results = []
success_count = 0
error_count = 0
for i, payload in enumerate(payloads):
try:
result = await client.invoke_function(
function_name=function_name, body=payload, method="POST"
)
results.append({"index": i, "success": True, "response": result})
success_count += 1
except Exception as e:
results.append({"index": i, "success": False, "error": str(e)})
error_count += 1
return json.dumps(
{
"success": True,
"function": function_name,
"total": len(payloads),
"succeeded": success_count,
"failed": error_count,
"results": results,
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)

View File

@@ -0,0 +1,484 @@
"""Storage Handler - manages Supabase Storage (buckets and files)"""
import base64
import json
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 [
{
"name": "list_buckets",
"method_name": "list_buckets",
"description": "List all storage buckets. Returns bucket names, public/private status, and settings.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "get_bucket",
"method_name": "get_bucket",
"description": "Get detailed information about a specific bucket including settings and limits.",
"schema": {
"type": "object",
"properties": {"bucket_id": {"type": "string", "description": "Bucket name/ID"}},
"required": ["bucket_id"],
},
"scope": "read",
},
{
"name": "create_bucket",
"method_name": "create_bucket",
"description": "Create a new storage bucket. Can be public (accessible without auth) or private.",
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Bucket name (lowercase, no spaces)",
"pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
},
"public": {
"type": "boolean",
"description": "Make bucket publicly accessible",
"default": False,
},
"file_size_limit": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Max file size in bytes (e.g., 52428800 for 50MB)",
},
"allowed_mime_types": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Allowed MIME types (e.g., ['image/png', 'image/jpeg'])",
},
},
"required": ["name"],
},
"scope": "admin",
},
{
"name": "update_bucket",
"method_name": "update_bucket",
"description": "Update bucket settings like public access, file size limit, or allowed MIME types.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket name/ID"},
"public": {
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"description": "Set public access",
},
"file_size_limit": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Max file size in bytes",
},
"allowed_mime_types": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Allowed MIME types",
},
},
"required": ["bucket_id"],
},
"scope": "admin",
},
{
"name": "delete_bucket",
"method_name": "delete_bucket",
"description": "Delete a storage bucket. Bucket must be empty first (use empty_bucket).",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket name/ID to delete"}
},
"required": ["bucket_id"],
},
"scope": "admin",
},
{
"name": "empty_bucket",
"method_name": "empty_bucket",
"description": "Delete all files in a bucket. Use before delete_bucket.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket name/ID to empty"}
},
"required": ["bucket_id"],
},
"scope": "admin",
},
{
"name": "list_files",
"method_name": "list_files",
"description": "List files in a bucket or folder path. Returns file names, sizes, and metadata.",
"schema": {
"type": "object",
"properties": {
"bucket": {"type": "string", "description": "Bucket name"},
"path": {
"type": "string",
"description": "Folder path prefix (empty for root)",
"default": "",
},
"limit": {
"type": "integer",
"description": "Max files to return",
"default": 100,
},
"offset": {
"type": "integer",
"description": "Offset for pagination",
"default": 0,
},
},
"required": ["bucket"],
},
"scope": "read",
},
{
"name": "upload_file",
"method_name": "upload_file",
"description": "Upload a file to storage. Content should be base64 encoded.",
"schema": {
"type": "object",
"properties": {
"bucket": {"type": "string", "description": "Bucket name"},
"path": {
"type": "string",
"description": "File path including filename (e.g., 'users/123/avatar.png')",
},
"content_base64": {
"type": "string",
"description": "File content encoded in base64",
},
"content_type": {
"type": "string",
"description": "MIME type (e.g., 'image/png', 'application/pdf')",
"default": "application/octet-stream",
},
"upsert": {
"type": "boolean",
"description": "Overwrite if file exists",
"default": False,
},
},
"required": ["bucket", "path", "content_base64"],
},
"scope": "write",
},
{
"name": "download_file",
"method_name": "download_file",
"description": "Download a file from storage. Returns base64 encoded content.",
"schema": {
"type": "object",
"properties": {
"bucket": {"type": "string", "description": "Bucket name"},
"path": {"type": "string", "description": "File path"},
},
"required": ["bucket", "path"],
},
"scope": "read",
},
{
"name": "delete_files",
"method_name": "delete_files",
"description": "Delete one or more files from a bucket.",
"schema": {
"type": "object",
"properties": {
"bucket": {"type": "string", "description": "Bucket name"},
"paths": {
"type": "array",
"items": {"type": "string"},
"description": "List of file paths to delete",
},
},
"required": ["bucket", "paths"],
},
"scope": "write",
},
{
"name": "move_file",
"method_name": "move_file",
"description": "Move or rename a file within the same bucket.",
"schema": {
"type": "object",
"properties": {
"bucket": {"type": "string", "description": "Bucket name"},
"from_path": {"type": "string", "description": "Current file path"},
"to_path": {"type": "string", "description": "New file path"},
},
"required": ["bucket", "from_path", "to_path"],
},
"scope": "write",
},
{
"name": "get_public_url",
"method_name": "get_public_url",
"description": "Get the public URL for a file. Only works for files in public buckets.",
"schema": {
"type": "object",
"properties": {
"bucket": {"type": "string", "description": "Bucket name (must be public)"},
"path": {"type": "string", "description": "File path"},
},
"required": ["bucket", "path"],
},
"scope": "read",
},
]
# =====================
# Storage Operations (12 tools)
# =====================
async def list_buckets(client: SupabaseClient) -> str:
"""List all storage buckets"""
try:
result = await client.list_buckets()
return json.dumps(
{
"success": True,
"count": len(result) if isinstance(result, list) else 0,
"buckets": result,
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.get_bucket(bucket_id)
return json.dumps({"success": True, "bucket": result}, indent=2, ensure_ascii=False)
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,
public: bool = False,
file_size_limit: int | None = None,
allowed_mime_types: list[str] | None = None,
) -> str:
"""Create a new bucket"""
try:
result = await client.create_bucket(
name=name,
public=public,
file_size_limit=file_size_limit,
allowed_mime_types=allowed_mime_types,
)
return json.dumps(
{"success": True, "message": f"Bucket '{name}' created successfully", "bucket": result},
indent=2,
ensure_ascii=False,
)
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,
public: bool | None = None,
file_size_limit: int | None = None,
allowed_mime_types: list[str] | None = None,
) -> str:
"""Update bucket settings"""
try:
result = await client.update_bucket(
bucket_id=bucket_id,
public=public,
file_size_limit=file_size_limit,
allowed_mime_types=allowed_mime_types,
)
return json.dumps(
{
"success": True,
"message": f"Bucket '{bucket_id}' updated successfully",
"bucket": result,
},
indent=2,
ensure_ascii=False,
)
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:
await client.delete_bucket(bucket_id)
return json.dumps(
{"success": True, "message": f"Bucket '{bucket_id}' deleted successfully"},
indent=2,
ensure_ascii=False,
)
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:
await client.empty_bucket(bucket_id)
return json.dumps(
{"success": True, "message": f"Bucket '{bucket_id}' emptied successfully"},
indent=2,
ensure_ascii=False,
)
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:
"""List files in bucket/path"""
try:
result = await client.list_files(bucket=bucket, path=path, limit=limit, offset=offset)
return json.dumps(
{
"success": True,
"bucket": bucket,
"path": path,
"count": len(result) if isinstance(result, list) else 0,
"files": result,
},
indent=2,
ensure_ascii=False,
)
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,
path: str,
content_base64: str,
content_type: str = "application/octet-stream",
upsert: bool = False,
) -> str:
"""Upload a file"""
try:
# Decode base64 content
try:
content = base64.b64decode(content_base64)
except Exception:
return json.dumps(
{"success": False, "error": "Invalid base64 content"}, indent=2, ensure_ascii=False
)
result = await client.upload_file(
bucket=bucket, path=path, content=content, content_type=content_type, upsert=upsert
)
# Build public URL if bucket is public
public_url = await client.get_public_url(bucket, path)
return json.dumps(
{
"success": True,
"message": f"File uploaded to {bucket}/{path}",
"size": len(content),
"content_type": content_type,
"public_url": public_url,
"result": result,
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.download_file(bucket, path)
if isinstance(result, dict) and "data" in result:
return json.dumps(
{
"success": True,
"bucket": bucket,
"path": path,
"content_type": result.get("content_type", "application/octet-stream"),
"size": result.get("size", 0),
"data_base64": result.get("data"),
},
indent=2,
ensure_ascii=False,
)
else:
return json.dumps(
{"success": True, "bucket": bucket, "path": path, "result": result},
indent=2,
ensure_ascii=False,
)
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:
result = await client.delete_files(bucket, paths)
return json.dumps(
{
"success": True,
"message": f"Deleted {len(paths)} file(s) from {bucket}",
"deleted_paths": paths,
"result": result,
},
indent=2,
ensure_ascii=False,
)
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:
result = await client.move_file(bucket, from_path, to_path)
return json.dumps(
{
"success": True,
"message": f"File moved from {from_path} to {to_path}",
"bucket": bucket,
"from_path": from_path,
"to_path": to_path,
"result": result,
},
indent=2,
ensure_ascii=False,
)
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:
url = await client.get_public_url(bucket, path)
return json.dumps(
{
"success": True,
"bucket": bucket,
"path": path,
"public_url": url,
"note": "URL only works if bucket is public",
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)

View File

@@ -0,0 +1,342 @@
"""System Handler - manages Supabase system operations (health, stats, info)"""
import json
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 [
{
"name": "health_check",
"method_name": "health_check",
"description": "Check health of all Supabase services (PostgREST, GoTrue, Storage). Returns status for each service.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "get_service_status",
"method_name": "get_service_status",
"description": "Get detailed status of a specific Supabase service (postgrest, gotrue, storage, postgres-meta).",
"schema": {
"type": "object",
"properties": {
"service": {
"type": "string",
"enum": ["postgrest", "gotrue", "storage", "postgres-meta"],
"description": "Service name to check",
}
},
"required": ["service"],
},
"scope": "read",
},
{
"name": "get_database_stats",
"method_name": "get_database_stats",
"description": "Get database statistics including table count, total size, connection info, and PostgreSQL version.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "get_storage_stats",
"method_name": "get_storage_stats",
"description": "Get storage statistics including bucket count, total files, and usage information.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "get_auth_stats",
"method_name": "get_auth_stats",
"description": "Get authentication statistics including total users, confirmed users, and provider breakdown.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "get_instance_info",
"method_name": "get_instance_info",
"description": "Get Supabase instance information including base URL and available API endpoints.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
]
async def health_check(client: SupabaseClient) -> str:
"""Check health of all Supabase services"""
try:
result = await client.health_check()
return json.dumps(
{
"success": True,
"healthy": result.get("healthy", False),
"instance_url": client.base_url,
"services": result.get("services", {}),
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps(
{"success": False, "healthy": False, "error": str(e), "instance_url": client.base_url},
indent=2,
ensure_ascii=False,
)
async def get_service_status(client: SupabaseClient, service: str) -> str:
"""Get status of a specific service"""
try:
status = {"service": service, "status": "unknown"}
if service == "postgrest":
# Check PostgREST by hitting root endpoint
try:
await client.request("GET", "/rest/v1/", use_service_role=True)
status["status"] = "ok"
status["endpoint"] = f"{client.base_url}/rest/v1/"
except Exception as e:
status["status"] = "error"
status["error"] = str(e)
elif service == "gotrue":
# Check GoTrue health endpoint
try:
result = await client.request("GET", "/auth/v1/health", use_service_role=False)
status["status"] = "ok"
status["endpoint"] = f"{client.base_url}/auth/v1/"
status["details"] = result
except Exception as e:
status["status"] = "error"
status["error"] = str(e)
elif service == "storage":
# Check Storage by listing buckets
try:
buckets = await client.list_buckets()
status["status"] = "ok"
status["endpoint"] = f"{client.base_url}/storage/v1/"
status["bucket_count"] = len(buckets) if isinstance(buckets, list) else 0
except Exception as e:
status["status"] = "error"
status["error"] = str(e)
elif service == "postgres-meta":
# Check postgres-meta by listing schemas
try:
schemas = await client.list_schemas()
status["status"] = "ok"
status["endpoint"] = f"{client.base_url}/pg/"
status["schema_count"] = len(schemas) if isinstance(schemas, list) else 0
except Exception as e:
status["status"] = "error"
status["error"] = str(e)
else:
status["status"] = "unknown"
status["error"] = f"Unknown service: {service}"
return json.dumps(
{"success": status["status"] == "ok", **status}, indent=2, ensure_ascii=False
)
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:
stats = {
"tables": 0,
"schemas": 0,
"extensions": 0,
"size": "unknown",
"version": "unknown",
}
# Get table count
try:
tables = await client.list_tables()
stats["tables"] = len(tables) if isinstance(tables, list) else 0
except:
pass
# Get schema count
try:
schemas = await client.list_schemas()
stats["schemas"] = len(schemas) if isinstance(schemas, list) else 0
except:
pass
# Get extension count
try:
extensions = await client.list_extensions()
stats["extensions"] = len(extensions) if isinstance(extensions, list) else 0
except:
pass
# Get database size and version
try:
size_result = await client.execute_sql(
"SELECT pg_size_pretty(pg_database_size(current_database())) as size, version() as version"
)
if isinstance(size_result, list) and len(size_result) > 0:
stats["size"] = size_result[0].get("size", "unknown")
stats["version"] = size_result[0].get("version", "unknown")
except:
pass
# Get connection info
try:
conn_result = await client.execute_sql(
"SELECT count(*) as connections FROM pg_stat_activity WHERE state = 'active'"
)
if isinstance(conn_result, list) and len(conn_result) > 0:
stats["active_connections"] = conn_result[0].get("connections", 0)
except:
pass
return json.dumps({"success": True, "database_stats": stats}, indent=2, ensure_ascii=False)
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:
stats = {
"buckets": 0,
"public_buckets": 0,
"private_buckets": 0,
"total_files": 0,
"bucket_details": [],
}
try:
buckets = await client.list_buckets()
if isinstance(buckets, list):
stats["buckets"] = len(buckets)
stats["public_buckets"] = sum(1 for b in buckets if b.get("public", False))
stats["private_buckets"] = stats["buckets"] - stats["public_buckets"]
# Get file counts per bucket
for bucket in buckets:
bucket_id = bucket.get("id") or bucket.get("name")
bucket_info = {
"name": bucket_id,
"public": bucket.get("public", False),
"file_count": 0,
}
try:
files = await client.list_files(bucket_id, limit=1000)
if isinstance(files, list):
bucket_info["file_count"] = len(files)
stats["total_files"] += len(files)
except:
pass
stats["bucket_details"].append(bucket_info)
except Exception as e:
stats["error"] = str(e)
return json.dumps({"success": True, "storage_stats": stats}, indent=2, ensure_ascii=False)
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:
stats = {
"total_users": 0,
"confirmed_users": 0,
"unconfirmed_users": 0,
"users_with_mfa": 0,
"providers": {},
}
try:
# Get users (first page)
users_response = await client.list_users(page=1, per_page=1000)
if isinstance(users_response, dict):
users = users_response.get("users", [])
else:
users = users_response if isinstance(users_response, list) else []
stats["total_users"] = len(users)
for user in users:
# Count confirmed/unconfirmed
if user.get("email_confirmed_at") or user.get("confirmed_at"):
stats["confirmed_users"] += 1
else:
stats["unconfirmed_users"] += 1
# Count MFA enabled
factors = user.get("factors", [])
if factors and len(factors) > 0:
stats["users_with_mfa"] += 1
# Count by provider
identities = user.get("identities", [])
for identity in identities:
provider = identity.get("provider", "email")
stats["providers"][provider] = stats["providers"].get(provider, 0) + 1
# If no identities, count as email
if not stats["providers"]:
stats["providers"]["email"] = stats["total_users"]
except Exception as e:
stats["error"] = str(e)
return json.dumps({"success": True, "auth_stats": stats}, indent=2, ensure_ascii=False)
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:
info = {
"base_url": client.base_url,
"api_endpoints": {
"rest": f"{client.base_url}/rest/v1",
"auth": f"{client.base_url}/auth/v1",
"storage": f"{client.base_url}/storage/v1",
"functions": f"{client.base_url}/functions/v1",
"realtime": f"{client.base_url}/realtime/v1",
"postgres_meta": f"{client.base_url}/pg",
},
"deployment_type": "self-hosted",
"services_available": [],
}
# Check which services are available
services_to_check = ["postgrest", "gotrue", "storage", "postgres-meta"]
for service in services_to_check:
try:
if service == "postgrest":
await client.request("GET", "/rest/v1/", use_service_role=True)
elif service == "gotrue":
await client.request("GET", "/auth/v1/health", use_service_role=False)
elif service == "storage":
await client.list_buckets()
elif service == "postgres-meta":
await client.list_schemas()
info["services_available"].append(service)
except:
pass
# Try to get PostgreSQL version
try:
version_result = await client.execute_sql("SELECT version()")
if isinstance(version_result, list) and len(version_result) > 0:
info["postgres_version"] = version_result[0].get("version", "unknown")
except:
pass
return json.dumps({"success": True, "instance_info": info}, indent=2, ensure_ascii=False)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)

156
plugins/supabase/plugin.py Normal file
View File

@@ -0,0 +1,156 @@
"""
Supabase Plugin - Self-Hosted Database & Backend Management
Complete Supabase Self-Hosted management through Kong gateway APIs.
Provides tools for Database (PostgREST), Auth (GoTrue), Storage,
Edge Functions, and Admin (postgres-meta) operations.
For Self-Hosted instances deployed on Coolify.
"""
from typing import Any
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.
Provides comprehensive Supabase management capabilities including:
- Database operations (PostgREST CRUD, RPC, count)
- Admin operations (postgres-meta: schemas, tables, extensions, RLS)
- Auth operations (GoTrue: users, MFA, invitations)
- Storage operations (buckets, files, public URLs)
- Edge Functions (invoke, deployment)
- System operations (health, stats)
Phase G.1: Database (18) + System (6) = 24 tools ✅
Phase G.2: Auth (14) + Storage (12) = 26 tools ✅
Phase G.3: Functions (8) + Admin (12) = 20 tools ✅
Total: 70 tools - Complete!
"""
@staticmethod
def get_plugin_name() -> str:
"""Return plugin type identifier"""
return "supabase"
@staticmethod
def get_required_config_keys() -> list[str]:
"""Return required configuration keys"""
return ["url", "anon_key", "service_role_key"]
def __init__(self, config: dict[str, Any], project_id: str | None = None):
"""
Initialize Supabase plugin with client.
Args:
config: Configuration dictionary containing:
- url: Supabase instance URL (Kong gateway)
- anon_key: Anonymous key (RLS protected)
- service_role_key: Service role key (bypasses RLS)
project_id: Optional project ID (auto-generated if not provided)
"""
super().__init__(config, project_id=project_id)
# Create Supabase API client
self.client = SupabaseClient(
base_url=config["url"],
anon_key=config["anon_key"],
service_role_key=config["service_role_key"],
)
@staticmethod
def get_tool_specifications() -> list[dict[str, Any]]:
"""
Return all tool specifications for ToolGenerator.
This method is called by ToolGenerator to create unified tools
with site parameter routing.
Returns:
List of tool specification dictionaries
"""
specs = []
# Phase G.1: Core (24 tools)
specs.extend(handlers.database.get_tool_specifications()) # 18 tools
specs.extend(handlers.system.get_tool_specifications()) # 6 tools
# Phase G.2: Auth & Storage (26 tools)
specs.extend(handlers.auth.get_tool_specifications()) # 14 tools
specs.extend(handlers.storage.get_tool_specifications()) # 12 tools
# Phase G.3: Functions & Admin (20 tools)
specs.extend(handlers.functions.get_tool_specifications()) # 8 tools
specs.extend(handlers.admin.get_tool_specifications()) # 12 tools
return specs
def __getattr__(self, name: str):
"""
Dynamically delegate method calls to appropriate handlers.
This allows ToolGenerator to call methods like plugin.query_table()
without explicitly defining each method.
Args:
name: Method name being called
Returns:
Handler function from the appropriate handler module
"""
# Try to find the method in handler modules
handler_modules = [
handlers.database,
handlers.system,
# Phase G.2
handlers.auth,
handlers.storage,
# Phase G.3
handlers.functions,
handlers.admin,
]
for handler_module in handler_modules:
if hasattr(handler_module, name):
func = getattr(handler_module, name)
# Create wrapper that passes self.client
async def wrapper(_func=func, **kwargs):
return await _func(self.client, **kwargs)
return wrapper
# Method not found in any handler
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
async def check_health(self) -> dict[str, Any]:
"""
Check if Supabase instance is accessible (internal use).
Note: This is named check_health to avoid shadowing the handler's
health_check function which is exposed as an MCP tool.
Returns:
Dict containing health check result
"""
try:
result = await self.client.health_check()
return {
"healthy": result.get("healthy", False),
"message": f"Supabase instance at {self.client.base_url} is {'accessible' if result.get('healthy') else 'not accessible'}",
}
except Exception as e:
return {"healthy": False, "message": f"Supabase health check failed: {str(e)}"}
async def health_check(self, **kwargs) -> str:
"""
Override BasePlugin.health_check to use handler function.
This ensures the MCP tool returns a JSON string, not a Dict.
"""
return await handlers.system.health_check(self.client)