Skip to main content
Glama

Supabase MCP Server

Apache 2.0
797
  • Apple
  • Linux
spec_manager.py8.86 kB
import json from enum import Enum from pathlib import Path from typing import Any import httpx from supabase_mcp.logger import logger # Constants SPEC_URL = "https://api.supabase.com/api/v1-json" LOCAL_SPEC_PATH = Path(__file__).parent / "specs" / "api_spec.json" class ApiDomain(str, Enum): """Enum of all possible domains in the Supabase Management API.""" ANALYTICS = "Analytics" AUTH = "Auth" DATABASE = "Database" DOMAINS = "Domains" EDGE_FUNCTIONS = "Edge Functions" ENVIRONMENTS = "Environments" OAUTH = "OAuth" ORGANIZATIONS = "Organizations" PROJECTS = "Projects" REST = "Rest" SECRETS = "Secrets" STORAGE = "Storage" @classmethod def list(cls) -> list[str]: """Return a list of all domain values.""" return [domain.value for domain in cls] class ApiSpecManager: """ Manages the OpenAPI specification for the Supabase Management API. Handles spec loading, caching, and validation. """ def __init__(self) -> None: self.spec: dict[str, Any] | None = None self._paths_cache: dict[str, dict[str, str]] | None = None self._domains_cache: list[str] | None = None async def _fetch_remote_spec(self) -> dict[str, Any] | None: """ Fetch latest OpenAPI spec from Supabase API. Returns None if fetch fails. """ try: async with httpx.AsyncClient() as client: response = await client.get(SPEC_URL) if response.status_code == 200: return response.json() logger.warning(f"Failed to fetch API spec: {response.status_code}") return None except Exception as e: logger.warning(f"Error fetching API spec: {e}") return None def _load_local_spec(self) -> dict[str, Any]: """ Load OpenAPI spec from local file. This is our fallback spec shipped with the server. """ try: with open(LOCAL_SPEC_PATH) as f: return json.load(f) except FileNotFoundError: logger.error(f"Local spec not found at {LOCAL_SPEC_PATH}") raise except json.JSONDecodeError as e: logger.error(f"Invalid JSON in local spec: {e}") raise async def get_spec(self) -> dict[str, Any]: """Retrieve the enriched spec.""" if self.spec is None: raw_spec = await self._fetch_remote_spec() if not raw_spec: # If remote fetch fails, use our fallback spec logger.info("Using fallback API spec") raw_spec = self._load_local_spec() self.spec = raw_spec return self.spec def get_all_paths_and_methods(self) -> dict[str, dict[str, str]]: """ Returns a dictionary of all paths and their methods with operation IDs. Returns: Dict[str, Dict[str, str]]: {path: {method: operationId}} """ if self._paths_cache is None: self._build_caches() return self._paths_cache or {} def get_paths_and_methods_by_domain(self, domain: str) -> dict[str, dict[str, str]]: """ Returns paths and methods within a specific domain (tag). Args: domain (str): The domain name (e.g., "Auth", "Projects"). Returns: Dict[str, Dict[str, str]]: {path: {method: operationId}} """ if self._paths_cache is None: self._build_caches() # Validate domain using enum try: valid_domain = ApiDomain(domain).value except ValueError as e: raise ValueError(f"Invalid domain: {domain}") from e domain_paths: dict[str, dict[str, str]] = {} if self.spec: for path, methods in self.spec.get("paths", {}).items(): for method, details in methods.items(): if valid_domain in details.get("tags", []): if path not in domain_paths: domain_paths[path] = {} domain_paths[path][method] = details.get("operationId", "") return domain_paths def get_all_domains(self) -> list[str]: """ Returns a list of all available domains (tags). Returns: List[str]: List of domain names. """ if self._domains_cache is None: self._build_caches() return self._domains_cache or [] def get_spec_for_path_and_method(self, path: str, method: str) -> dict[str, Any] | None: """ Returns the full specification for a given path and HTTP method. Args: path (str): The API path (e.g., "/v1/projects"). method (str): The HTTP method (e.g., "get", "post"). Returns: Optional[Dict[str, Any]]: The full spec for the operation, or None if not found. """ if self.spec is None: return None path_spec = self.spec.get("paths", {}).get(path) if path_spec: return path_spec.get(method.lower()) # Ensure lowercase method return None def get_spec_part(self, part: str, *args: str | int) -> Any: """ Safely retrieves a nested part of the OpenAPI spec. Args: part: The top-level key (e.g., 'paths', 'components'). *args: Subsequent keys or indices to traverse the spec. Returns: The value at the specified location in the spec, or None if not found. """ if self.spec is None: return None current = self.spec.get(part) for key in args: if isinstance(current, dict) and key in current: current = current[key] elif isinstance(current, list) and isinstance(key, int) and 0 <= key < len(current): current = current[key] else: return None # Key not found or invalid index return current def _build_caches(self) -> None: """ Build internal caches for faster lookups. This populates _paths_cache and _domains_cache. """ if self.spec is None: logger.error("Cannot build caches: OpenAPI spec not loaded") return # Build paths cache paths_cache: dict[str, dict[str, str]] = {} domains_set = set() for path, methods in self.spec.get("paths", {}).items(): for method, details in methods.items(): # Add to paths cache if path not in paths_cache: paths_cache[path] = {} paths_cache[path][method] = details.get("operationId", "") # Collect domains (tags) for tag in details.get("tags", []): domains_set.add(tag) self._paths_cache = paths_cache self._domains_cache = sorted(list(domains_set)) # Example usage (assuming you have an instance of ApiSpecManager called 'spec_manager'): async def main() -> None: """Test function to demonstrate ApiSpecManager functionality.""" # Create a new instance of ApiSpecManager spec_manager = ApiSpecManager() # Load the spec await spec_manager.get_spec() # Print the path to help debug print(f"Looking for spec at: {LOCAL_SPEC_PATH}") # 1. Get all domains all_domains = spec_manager.get_all_domains() print("\nAll Domains:") print(all_domains) # 2. Get all paths and methods all_paths = spec_manager.get_all_paths_and_methods() print("\nAll Paths and Methods (sample):") # Just print a few to avoid overwhelming output for i, (path, methods) in enumerate(all_paths.items()): if i >= 5: # Limit to 5 paths break print(f" {path}:") for method, operation_id in methods.items(): print(f" {method}: {operation_id}") # 3. Get paths and methods for the "Edge Functions" domain edge_paths = spec_manager.get_paths_and_methods_by_domain("Edge Functions") print("\nEdge Functions Paths and Methods:") for path, methods in edge_paths.items(): print(f" {path}:") for method, operation_id in methods.items(): print(f" {method}: {operation_id}") # 4. Get the full spec for a specific path and method path = "/v1/projects/{ref}/functions" method = "GET" full_spec = spec_manager.get_spec_for_path_and_method(path, method) print(f"\nFull Spec for {method} {path}:") if full_spec: print(json.dumps(full_spec, indent=2)[:500] + "...") # Truncate for readability else: print("Spec not found for this path/method") if __name__ == "__main__": import asyncio asyncio.run(main())

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/alexander-zuev/supabase-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server