"""Configuration management for Schwab MCP Server."""
import json
from pathlib import Path
from typing import Optional
from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Application settings loaded from environment variables or token file."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
# Schwab API credentials (optional if stored in token file)
schwab_client_id: Optional[str] = None
schwab_client_secret: Optional[str] = None
schwab_callback_url: str = "https://127.0.0.1:8182/callback"
# Token storage path
schwab_token_path: Path = Path.home() / ".schwab-mcp" / "token.json"
# Optional settings
log_level: str = "INFO"
schwab_default_account: Optional[str] = None
schwab_timeout: int = 30
@model_validator(mode="after")
def load_credentials_from_token_file(self) -> "Settings":
"""Populate missing client credentials from the token file if present."""
# Always expand ~ to ensure consistent path handling
self.schwab_token_path = Path(self.schwab_token_path).expanduser()
if self.schwab_client_id and self.schwab_client_secret:
return self
try:
with open(self.schwab_token_path) as f:
data = json.load(f)
self.schwab_client_id = self.schwab_client_id or data.get("client_id")
self.schwab_client_secret = self.schwab_client_secret or data.get(
"client_secret"
)
except FileNotFoundError:
pass
except json.JSONDecodeError as exc:
raise ValueError(
f"Token file at {self.schwab_token_path} is not valid JSON"
) from exc
# Don't raise error here - let it fail gracefully when credentials are actually needed
# This allows the MCP server to start and list tools even without credentials
return self
def validate_credentials(self) -> None:
"""Validate that credentials are available. Raises ValueError if not."""
if not self.schwab_client_id or not self.schwab_client_secret:
raise ValueError(
"Missing Schwab client credentials. "
"Set SCHWAB_CLIENT_ID/SCHWAB_CLIENT_SECRET or add client_id/client_secret "
f"to the token file at {self.schwab_token_path}."
)
def get_settings() -> Settings:
"""Get application settings."""
return Settings()
# Global settings instance (lazy loaded)
_settings: Optional[Settings] = None
def settings() -> Settings:
"""Get or create the global settings instance."""
global _settings
if _settings is None:
_settings = get_settings()
return _settings