Skip to main content
Glama
config.py5.37 kB
"""Configuration management for ClickUp MCP server.""" import json import os from pathlib import Path from typing import Any, Dict, Optional from platformdirs import user_config_dir from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator class ConfigError(Exception): """Configuration related errors.""" pass class IDPattern(BaseModel): """Custom ID pattern configuration.""" prefix: str description: str class ConfigModel(BaseModel): """Configuration model for the server.""" api_key: SecretStr default_workspace_id: Optional[str] = None default_team_id: Optional[str] = None cache_ttl: int = Field(default=300, description="Cache TTL in seconds") id_patterns: Dict[str, str] = Field( default_factory=lambda: {"gh": "GitHub Issues", "GH": "GitHub Issues"}, description="Custom ID patterns like {'gh': 'GitHub Issues'}", ) @field_validator("api_key") @classmethod def validate_api_key(cls, v: SecretStr) -> SecretStr: """Validate API key format.""" key = v.get_secret_value() if not key or len(key) < 10: raise ValueError("Invalid API key format") return v class Config(BaseModel): """Main configuration class with multiple source support.""" model_config = ConfigDict( extra="ignore", ) api_key: str = Field(description="ClickUp API key") default_workspace_id: Optional[str] = None default_team_id: Optional[str] = None cache_ttl: int = 300 id_patterns: Dict[str, str] = Field( default_factory=lambda: {"gh": "GitHub Issues", "GH": "GitHub Issues"} ) def __init__(self, **kwargs: Any) -> None: """Initialize config from multiple sources.""" # Try to load from config files first config_data = self._load_from_files() # Add environment variables with our prefix env_data = self._get_filtered_env_vars() config_data.update(env_data) # Merge with any provided kwargs (highest priority) config_data.update(kwargs) # Use BaseModel init to avoid BaseSettings environment processing super().__init__(**config_data) # Validate we have required fields if not self.api_key: raise ConfigError( "No API key found. Please configure CLICKUP_MCP_API_KEY or create a config file." ) def _load_from_files(self) -> Dict[str, Any]: """Load configuration from standard file locations.""" config_locations = [ # XDG standard location (preferred) Path.home() / ".config" / "clickup-mcp" / "config.json", # Platform-specific location (e.g., ~/Library/Application Support on macOS) Path(user_config_dir("clickup-mcp")) / "config.json", # Fallback home directory location Path.home() / ".clickup-mcp" / "config.json", ] # Also check XDG_CONFIG_HOME if set xdg_config = os.environ.get("XDG_CONFIG_HOME") if xdg_config: config_locations.insert(1, Path(xdg_config) / "clickup-mcp" / "config.json") for config_path in config_locations: if config_path.exists(): try: with open(config_path, "r") as f: data = json.load(f) # Validate with pydantic model ConfigModel(**data) return data except json.JSONDecodeError as e: raise ConfigError(f"Invalid JSON in {config_path}: {e}") from e except Exception as e: raise ConfigError(f"Error loading config from {config_path}: {e}") from e return {} def _get_filtered_env_vars(self) -> Dict[str, Any]: """Get environment variables with our prefix only.""" env_data = {} prefix = "CLICKUP_MCP_" # Only process environment variables with our specific prefix for key, value in os.environ.items(): if key.startswith(prefix) and value: # Remove prefix and convert to lowercase for field mapping field_name = key[len(prefix) :].lower() env_data[field_name] = value return env_data def save_to_file(self, path: Optional[Path] = None) -> None: """Save current configuration to file.""" if path is None: path = Path(user_config_dir("clickup-mcp")) / "config.json" path.parent.mkdir(parents=True, exist_ok=True) config_data = { "api_key": self.api_key, } if self.default_workspace_id: config_data["default_workspace_id"] = self.default_workspace_id if self.default_team_id: config_data["default_team_id"] = self.default_team_id if self.id_patterns: config_data["id_patterns"] = self.id_patterns if self.cache_ttl != 300: config_data["cache_ttl"] = self.cache_ttl with open(path, "w") as f: json.dump(config_data, f, indent=2) @property def headers(self) -> Dict[str, str]: """Get headers for ClickUp API requests.""" return { "Authorization": self.api_key, "Content-Type": "application/json", }

Latest Blog Posts

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/DiversioTeam/clickup-mcp'

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