Complete MCP configuration - clean, immutable, type-safe. This is the source of truth loaded from config files. RuntimeConfig wraps this with CLI/env overrides.
| 143 | |
| 144 | |
| 145 | class MCPConfig(BaseModel): |
| 146 | """Complete MCP configuration - clean, immutable, type-safe. |
| 147 | |
| 148 | This is the source of truth loaded from config files. |
| 149 | RuntimeConfig wraps this with CLI/env overrides. |
| 150 | """ |
| 151 | |
| 152 | # Provider/Model defaults (no more magic strings!) |
| 153 | default_provider: str = DEFAULT_PROVIDER |
| 154 | default_model: str = DEFAULT_MODEL |
| 155 | |
| 156 | # UI (theme names validated by chuk-term) |
| 157 | theme: str = DEFAULT_THEME # default|dark|light|minimal|terminal |
| 158 | verbose: bool = DEFAULT_VERBOSE |
| 159 | |
| 160 | # Configurations |
| 161 | timeouts: TimeoutConfig = Field(default_factory=TimeoutConfig) |
| 162 | tools: ToolConfig = Field(default_factory=ToolConfig) |
| 163 | token_storage: TokenStorageConfig = Field(default_factory=TokenStorageConfig) |
| 164 | |
| 165 | # Servers - kept as dict for flexibility but typed |
| 166 | # Use alias to support both 'servers' and 'mcpServers' from config files |
| 167 | servers: dict[str, Any] = Field(default_factory=dict, alias="mcpServers") |
| 168 | |
| 169 | model_config = {"frozen": True, "populate_by_name": True} |
| 170 | |
| 171 | @classmethod |
| 172 | async def load_async(cls, config_path: Path) -> MCPConfig: |
| 173 | """Async load from file (future-proof for async I/O).""" |
| 174 | import asyncio |
| 175 | import json |
| 176 | |
| 177 | if not config_path.exists(): |
| 178 | return cls() |
| 179 | |
| 180 | # Use asyncio for file I/O |
| 181 | loop = asyncio.get_event_loop() |
| 182 | data = await loop.run_in_executor(None, config_path.read_text) |
| 183 | parsed = json.loads(data) |
| 184 | |
| 185 | return cls.model_validate(parsed) # type: ignore[no-any-return] |
| 186 | |
| 187 | @classmethod |
| 188 | def load_sync(cls, config_path: Path) -> MCPConfig: |
| 189 | """Synchronous load (for backward compat during transition).""" |
| 190 | import json |
| 191 | |
| 192 | if not config_path.exists(): |
| 193 | return cls() |
| 194 | |
| 195 | data = json.loads(config_path.read_text()) |
| 196 | return cls.model_validate(data) # type: ignore[no-any-return] |
| 197 | |
| 198 | @classmethod |
| 199 | def load_from_file(cls, config_path: Path) -> MCPConfig: |
| 200 | """Alias for load_sync for backward compatibility.""" |
| 201 | return cls.load_sync(config_path) |
| 202 |
no outgoing calls