"""Configuration management for Canvas MCP server."""
import os
import sys
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
_INVALID_INT_ENV_VARS: dict[str, str] = {}
def _bool_env(name: str, default: bool) -> bool:
value = os.getenv(name)
if value is None:
return default
return value.strip().lower() == "true"
def _int_env(name: str, default: int) -> int:
value = os.getenv(name)
if value is None or value.strip() == "":
return default
try:
return int(value)
except ValueError:
_INVALID_INT_ENV_VARS[name] = value
return default
class Config:
"""Configuration class for Canvas MCP server."""
def __init__(self) -> None:
# Required configuration
self.canvas_api_token = os.getenv("CANVAS_API_TOKEN", "")
self.canvas_api_url = os.getenv("CANVAS_API_URL", "https://canvas.illinois.edu/api/v1")
# Optional configuration with defaults
self.mcp_server_name = os.getenv("MCP_SERVER_NAME", "canvas-api")
self.debug = _bool_env("DEBUG", False)
self.api_timeout = _int_env("API_TIMEOUT", 30)
self.cache_ttl = _int_env("CACHE_TTL", 300)
self.max_concurrent_requests = _int_env("MAX_CONCURRENT_REQUESTS", 10)
# Development configuration
self.log_level = os.getenv("LOG_LEVEL", "INFO").upper()
self.log_api_requests = _bool_env("LOG_API_REQUESTS", False)
# Privacy and security configuration
self.enable_data_anonymization = _bool_env("ENABLE_DATA_ANONYMIZATION", True)
self.anonymization_debug = _bool_env("ANONYMIZATION_DEBUG", False)
# Code execution sandbox configuration (best-effort by default)
self.enable_ts_sandbox = _bool_env("ENABLE_TS_SANDBOX", False)
self.ts_sandbox_mode = os.getenv("TS_SANDBOX_MODE", "auto").lower()
self.ts_sandbox_block_outbound_network = _bool_env("TS_SANDBOX_BLOCK_OUTBOUND_NETWORK", False)
self.ts_sandbox_allowlist_hosts = os.getenv("TS_SANDBOX_ALLOWLIST_HOSTS", "")
self.ts_sandbox_cpu_limit = _int_env("TS_SANDBOX_CPU_LIMIT", 0)
self.ts_sandbox_memory_limit_mb = _int_env("TS_SANDBOX_MEMORY_LIMIT_MB", 0)
self.ts_sandbox_timeout_sec = _int_env("TS_SANDBOX_TIMEOUT_SEC", 0)
self.ts_sandbox_container_image = os.getenv("TS_SANDBOX_CONTAINER_IMAGE", "node:20-alpine")
# Optional metadata
self.institution_name = os.getenv("INSTITUTION_NAME", "")
self.timezone = os.getenv("TIMEZONE", "UTC")
@property
def api_base_url(self) -> str:
"""Legacy compatibility for API_BASE_URL."""
return self.canvas_api_url
@property
def api_token(self) -> str:
"""Legacy compatibility for API_TOKEN."""
return self.canvas_api_token
# Global configuration instance
_config: Config | None = None
def get_config() -> Config:
"""Get the global configuration instance."""
global _config
if _config is None:
_config = Config()
return _config
def validate_config() -> bool:
"""Validate that required configuration is present."""
config = get_config()
unimplemented_env_vars = {
"TOKEN_STORAGE_BACKEND": "token storage backend selection is not enforced yet",
"TOKEN_ENVELOPE_KEY_SOURCE": "token envelope encryption is not enforced yet",
"TOKEN_STARTUP_VALIDATION": "token startup validation is not enforced yet",
"MCP_CLIENT_AUTH_MODE": "MCP client authentication is not implemented for stdio transport",
"MCP_CLIENT_API_KEY_REQUIRED": "MCP client authentication is not implemented for stdio transport",
"MCP_CLIENT_CERT_AUTHORITY": "MCP client authentication is not implemented for stdio transport",
"LOG_REDACT_PII": "PII redaction is not enforced yet",
"LOG_ROTATION_DAYS": "log rotation is not enforced yet",
"LOG_ACCESS_EVENTS": "access/audit logging is not implemented yet",
"LOG_EXECUTION_EVENTS": "execution event logging is not implemented yet",
"LOG_RETENTION_DAYS": "log retention is not enforced yet",
"LOG_DESTINATION": "log destinations are not configurable yet",
"SIEM_FORWARDING_ENABLED": "SIEM forwarding is not implemented yet",
"MCP_BIND_HOST": "MCP uses stdio transport and does not bind network sockets",
"MCP_BIND_PORT": "MCP uses stdio transport and does not bind network sockets",
"FIREWALL_HINT": "firewall hints are documentation-only",
}
if not config.canvas_api_token:
print("Error: CANVAS_API_TOKEN environment variable is required", file=sys.stderr)
print("Please set it to your Canvas API token in your .env file", file=sys.stderr)
return False
if not config.canvas_api_url:
print("Error: CANVAS_API_URL environment variable is required", file=sys.stderr)
print("Please set it to your Canvas API URL in your .env file", file=sys.stderr)
return False
if not config.canvas_api_url.endswith("/api/v1"):
print("Warning: CANVAS_API_URL should end with '/api/v1'", file=sys.stderr)
print(f"Current URL: {config.canvas_api_url}", file=sys.stderr)
if config.ts_sandbox_mode not in {"auto", "local", "container"}:
print(
"Warning: TS_SANDBOX_MODE should be one of auto, local, container; "
f"defaulting to 'auto' (got '{config.ts_sandbox_mode}')",
file=sys.stderr
)
for env_name, env_value in _INVALID_INT_ENV_VARS.items():
print(
f"Warning: {env_name} expects an integer; using default value "
f"(got '{env_value}')",
file=sys.stderr
)
for env_name, note in unimplemented_env_vars.items():
if os.getenv(env_name):
print(
f"Warning: {env_name} is set but {note}.",
file=sys.stderr
)
return True
# Legacy compatibility - these will be used by existing code
API_BASE_URL = get_config().api_base_url
API_TOKEN = get_config().api_token