settings.py•7.67 kB
import os
from pathlib import Path
from typing import Literal
from pydantic import Field, ValidationInfo, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from supabase_mcp.logger import logger
SUPPORTED_REGIONS = Literal[
"us-west-1", # West US (North California)
"us-east-1", # East US (North Virginia)
"us-east-2", # East US (Ohio)
"ca-central-1", # Canada (Central)
"eu-west-1", # West EU (Ireland)
"eu-west-2", # West Europe (London)
"eu-west-3", # West EU (Paris)
"eu-central-1", # Central EU (Frankfurt)
"eu-central-2", # Central Europe (Zurich)
"eu-north-1", # North EU (Stockholm)
"ap-south-1", # South Asia (Mumbai)
"ap-southeast-1", # Southeast Asia (Singapore)
"ap-northeast-1", # Northeast Asia (Tokyo)
"ap-northeast-2", # Northeast Asia (Seoul)
"ap-southeast-2", # Oceania (Sydney)
"sa-east-1", # South America (São Paulo)
]
def find_config_file(env_file: str = ".env") -> str | None:
"""Find the specified env file in order of precedence:
1. Current working directory (where command is run)
2. Global config:
- Windows: %APPDATA%/supabase-mcp/{env_file}
- macOS/Linux: ~/.config/supabase-mcp/{env_file}
Args:
env_file: The name of the environment file to look for (default: ".env")
Returns:
The path to the found config file, or None if not found
"""
# 1. Check current directory
cwd_config = Path.cwd() / env_file
if cwd_config.exists():
return str(cwd_config)
# 2. Check global config
home = Path.home()
if os.name == "nt": # Windows
global_config = Path(os.environ.get("APPDATA", "")) / "supabase-mcp" / ".env"
else: # macOS/Linux
global_config = home / ".config" / "supabase-mcp" / ".env"
if global_config.exists():
logger.error(
f"DEPRECATED: {global_config} is deprecated and will be removed in a future release. "
"Use your IDE's native .json config file to configure access to MCP."
)
return str(global_config)
return None
class Settings(BaseSettings):
"""Initializes settings for Supabase MCP server."""
supabase_project_ref: str = Field(
default="127.0.0.1:54322", # Local Supabase default
description="Supabase project ref - Must be 20 chars for remote projects, can be local address for development",
alias="SUPABASE_PROJECT_REF",
)
supabase_db_password: str | None = Field(
default=None, # Will be validated based on project_ref
description="Supabase database password - Required for remote projects, defaults to 'postgres' for local",
alias="SUPABASE_DB_PASSWORD",
)
supabase_region: str = Field(
default="us-east-1", # East US (North Virginia) - Supabase's default region
description="Supabase region for connection",
alias="SUPABASE_REGION",
)
supabase_access_token: str | None = Field(
default=None,
description="Optional personal access token for accessing Supabase Management API",
alias="SUPABASE_ACCESS_TOKEN",
)
supabase_service_role_key: str | None = Field(
default=None,
description="Optional service role key for accessing Python SDK",
alias="SUPABASE_SERVICE_ROLE_KEY",
)
supabase_api_url: str = Field(
default="https://api.supabase.com",
description="Supabase API URL",
)
query_api_key: str = Field(
default="test-key",
description="TheQuery.dev API key",
alias="QUERY_API_KEY",
)
query_api_url: str = Field(
default="https://api.thequery.dev/v1",
description="TheQuery.dev API URL",
alias="QUERY_API_URL",
)
@field_validator("supabase_region")
@classmethod
def validate_region(cls, v: str, info: ValidationInfo) -> str:
"""Validate that the region is supported by Supabase."""
# Get the project_ref from the values
values = info.data
project_ref = values.get("supabase_project_ref", "")
# If this is a remote project and region is the default
if not project_ref.startswith("127.0.0.1") and v == "us-east-1" and "SUPABASE_REGION" not in os.environ:
logger.warning(
"You're connecting to a remote Supabase project but haven't specified a region. "
"Using default 'us-east-1', which may cause 'Tenant or user not found' errors if incorrect. "
"Please set the correct SUPABASE_REGION in your configuration."
)
# Validate that the region is supported
if v not in SUPPORTED_REGIONS.__args__:
supported = "\n - ".join([""] + list(SUPPORTED_REGIONS.__args__))
raise ValueError(f"Region '{v}' is not supported. Supported regions are:{supported}")
return v
@field_validator("supabase_project_ref")
@classmethod
def validate_project_ref(cls, v: str) -> str:
"""Validate the project ref format."""
if v.startswith("127.0.0.1"):
# Local development - allow default format
return v
# Remote project - must be 20 chars
if len(v) != 20:
logger.error("Invalid Supabase project ref format")
raise ValueError(
"Invalid Supabase project ref format. "
"Remote project refs must be exactly 20 characters long. "
f"Got {len(v)} characters instead."
)
return v
@field_validator("supabase_db_password")
@classmethod
def validate_db_password(cls, v: str | None, info: ValidationInfo) -> str:
"""Validate database password based on project type."""
project_ref = info.data.get("supabase_project_ref", "")
# For local development, allow default password
if project_ref.startswith("127.0.0.1"):
return v or "postgres" # Default to postgres for local
# For remote projects, password is required
if not v:
logger.error("SUPABASE_DB_PASSWORD is required when connecting to a remote instance")
raise ValueError(
"Database password is required for remote Supabase projects. "
"Please set SUPABASE_DB_PASSWORD in your environment variables."
)
return v
@classmethod
def with_config(cls, config_file: str | None = None) -> "Settings":
"""Create Settings with a specific config file.
Args:
config_file: Path to .env file to use, or None for no config file
"""
# Create a new Settings class with the specific config
class SettingsWithConfig(cls):
model_config = SettingsConfigDict(env_file=config_file, env_file_encoding="utf-8")
instance = SettingsWithConfig()
# Log configuration source and precedence - simplified to a single clear message
env_vars_present = any(var in os.environ for var in ["SUPABASE_PROJECT_REF", "SUPABASE_DB_PASSWORD"])
if env_vars_present and config_file:
logger.info(f"Using environment variables (highest precedence) over config file: {config_file}")
elif env_vars_present:
logger.info("Using environment variables for configuration")
elif config_file:
logger.info(f"Using settings from config file: {config_file}")
else:
logger.info("Using default settings (local development)")
return instance
# Module-level singleton - maintains existing interface
settings = Settings.with_config(find_config_file())