|
| 1 | +# MCP Host Configuration Architecture |
| 2 | + |
| 3 | +This article covers: |
| 4 | + |
| 5 | +- Unified Adapter Architecture for MCP host configuration |
| 6 | +- Adapter pattern for host-specific validation and serialization |
| 7 | +- Unified data model (`MCPServerConfig`) |
| 8 | +- Extension points for adding new host platforms |
| 9 | +- Integration with backup and environment systems |
| 10 | + |
| 11 | +## Overview |
| 12 | + |
| 13 | +The MCP host configuration system manages Model Context Protocol server configurations across multiple host platforms (Claude Desktop, VS Code, Cursor, Gemini, Kiro, Codex, LM Studio). It uses the **Unified Adapter Architecture**: a single data model with host-specific adapters for validation and serialization. |
| 14 | + |
| 15 | +> **Adding a new host?** See the [Implementation Guide](../implementation_guides/mcp_host_configuration_extension.md) for step-by-step instructions. |
| 16 | +
|
| 17 | +## Core Architecture |
| 18 | + |
| 19 | +### Unified Adapter Pattern |
| 20 | + |
| 21 | +The architecture separates concerns into three layers: |
| 22 | + |
| 23 | +``` |
| 24 | +┌─────────────────────────────────────────────────────────────────┐ |
| 25 | +│ CLI Layer │ |
| 26 | +│ Creates MCPServerConfig with all user-provided fields │ |
| 27 | +└─────────────────────────────────┬───────────────────────────────┘ |
| 28 | + │ |
| 29 | + ▼ |
| 30 | +┌─────────────────────────────────────────────────────────────────┐ |
| 31 | +│ Adapter Layer │ |
| 32 | +│ Validates + serializes to host-specific format │ |
| 33 | +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ |
| 34 | +│ │ Claude │ │ VSCode │ │ Gemini │ │ Kiro │ ... │ |
| 35 | +│ │ Adapter │ │ Adapter │ │ Adapter │ │ Adapter │ │ |
| 36 | +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ |
| 37 | +└─────────────────────────────────┬───────────────────────────────┘ |
| 38 | + │ |
| 39 | + ▼ |
| 40 | +┌─────────────────────────────────────────────────────────────────┐ |
| 41 | +│ Strategy Layer │ |
| 42 | +│ Handles file I/O (read/write configuration files) │ |
| 43 | +└─────────────────────────────────────────────────────────────────┘ |
| 44 | +``` |
| 45 | + |
| 46 | +**Benefits:** |
| 47 | + |
| 48 | +- Single unified data model accepts all fields |
| 49 | +- Adapters declaratively define supported fields per host |
| 50 | +- No inheritance hierarchies or model conversion methods |
| 51 | +- Easy addition of new hosts (3 steps instead of 10) |
| 52 | + |
| 53 | +### Unified Data Model |
| 54 | + |
| 55 | +`MCPServerConfig` contains ALL possible fields from ALL hosts: |
| 56 | + |
| 57 | +```python |
| 58 | +class MCPServerConfig(BaseModel): |
| 59 | + """Unified model containing ALL possible fields.""" |
| 60 | + model_config = ConfigDict(extra="allow") |
| 61 | + |
| 62 | + # Hatch metadata (never serialized) |
| 63 | + name: Optional[str] = None |
| 64 | + |
| 65 | + # Transport fields |
| 66 | + command: Optional[str] = None # stdio transport |
| 67 | + url: Optional[str] = None # sse transport |
| 68 | + httpUrl: Optional[str] = None # http transport (Gemini) |
| 69 | + |
| 70 | + # Universal fields (all hosts) |
| 71 | + args: Optional[List[str]] = None |
| 72 | + env: Optional[Dict[str, str]] = None |
| 73 | + headers: Optional[Dict[str, str]] = None |
| 74 | + type: Optional[Literal["stdio", "sse", "http"]] = None |
| 75 | + |
| 76 | + # Host-specific fields |
| 77 | + envFile: Optional[str] = None # VSCode/Cursor |
| 78 | + disabled: Optional[bool] = None # Kiro |
| 79 | + trust: Optional[bool] = None # Gemini |
| 80 | + # ... additional fields per host |
| 81 | +``` |
| 82 | + |
| 83 | +**Design principles:** |
| 84 | + |
| 85 | +- `extra="allow"` for forward compatibility with unknown fields |
| 86 | +- Adapters handle validation (not the model) |
| 87 | +- `name` field is Hatch metadata, never serialized to host configs |
| 88 | + |
| 89 | +## Key Components |
| 90 | + |
| 91 | +### AdapterRegistry |
| 92 | + |
| 93 | +Central registry mapping host names to adapter instances: |
| 94 | + |
| 95 | +```python |
| 96 | +from hatch.mcp_host_config.adapters import get_adapter, AdapterRegistry |
| 97 | + |
| 98 | +# Get adapter for a specific host |
| 99 | +adapter = get_adapter("claude-desktop") |
| 100 | + |
| 101 | +# Or use registry directly |
| 102 | +registry = AdapterRegistry() |
| 103 | +adapter = registry.get_adapter("gemini") |
| 104 | +supported = registry.get_supported_hosts() # List all hosts |
| 105 | +``` |
| 106 | + |
| 107 | +**Supported hosts:** |
| 108 | + |
| 109 | +- `claude-desktop`, `claude-code` |
| 110 | +- `vscode`, `cursor`, `lmstudio` |
| 111 | +- `gemini`, `kiro`, `codex` |
| 112 | + |
| 113 | +### BaseAdapter Protocol |
| 114 | + |
| 115 | +All adapters implement this interface: |
| 116 | + |
| 117 | +```python |
| 118 | +class BaseAdapter(ABC): |
| 119 | + @property |
| 120 | + @abstractmethod |
| 121 | + def host_name(self) -> str: |
| 122 | + """Return host identifier (e.g., 'claude-desktop').""" |
| 123 | + ... |
| 124 | + |
| 125 | + @abstractmethod |
| 126 | + def get_supported_fields(self) -> FrozenSet[str]: |
| 127 | + """Return fields this host accepts.""" |
| 128 | + ... |
| 129 | + |
| 130 | + @abstractmethod |
| 131 | + def validate(self, config: MCPServerConfig) -> None: |
| 132 | + """Validate config, raise AdapterValidationError if invalid.""" |
| 133 | + ... |
| 134 | + |
| 135 | + @abstractmethod |
| 136 | + def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: |
| 137 | + """Convert config to host's expected format.""" |
| 138 | + ... |
| 139 | +``` |
| 140 | + |
| 141 | +### Field Constants |
| 142 | + |
| 143 | +Field support is defined in `fields.py`: |
| 144 | + |
| 145 | +```python |
| 146 | +# Universal fields (all hosts) |
| 147 | +UNIVERSAL_FIELDS = frozenset({"command", "args", "env", "url", "headers"}) |
| 148 | + |
| 149 | +# Host-specific field sets |
| 150 | +CLAUDE_FIELDS = UNIVERSAL_FIELDS | frozenset({"type"}) |
| 151 | +VSCODE_FIELDS = CLAUDE_FIELDS | frozenset({"envFile", "inputs"}) |
| 152 | +GEMINI_FIELDS = UNIVERSAL_FIELDS | frozenset({"httpUrl", "timeout", "trust", ...}) |
| 153 | +KIRO_FIELDS = UNIVERSAL_FIELDS | frozenset({"disabled", "autoApprove", ...}) |
| 154 | +``` |
| 155 | + |
| 156 | +## Field Support Matrix |
| 157 | + |
| 158 | +| Field | Claude | VSCode | Cursor | Gemini | Kiro | Codex | |
| 159 | +|-------|--------|--------|--------|--------|------|-------| |
| 160 | +| command, args, env | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| 161 | +| url, headers | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| 162 | +| type | ✓ | ✓ | ✓ | - | - | - | |
| 163 | +| envFile | - | ✓ | ✓ | - | - | - | |
| 164 | +| inputs | - | ✓ | - | - | - | - | |
| 165 | +| httpUrl | - | - | - | ✓ | - | - | |
| 166 | +| trust, timeout | - | - | - | ✓ | - | - | |
| 167 | +| disabled, autoApprove | - | - | - | - | ✓ | - | |
| 168 | +| enabled, enabled_tools | - | - | - | - | - | ✓ | |
| 169 | + |
| 170 | +## Integration Points |
| 171 | + |
| 172 | +### Adapter Integration |
| 173 | + |
| 174 | +Every adapter integrates with the validation and serialization system: |
| 175 | + |
| 176 | +```python |
| 177 | +from hatch.mcp_host_config.adapters import get_adapter |
| 178 | +from hatch.mcp_host_config import MCPServerConfig |
| 179 | + |
| 180 | +# Create unified config |
| 181 | +config = MCPServerConfig( |
| 182 | + name="my-server", |
| 183 | + command="python", |
| 184 | + args=["server.py"], |
| 185 | + env={"DEBUG": "true"}, |
| 186 | +) |
| 187 | + |
| 188 | +# Validate and serialize for specific host |
| 189 | +adapter = get_adapter("claude-desktop") |
| 190 | +adapter.validate(config) # Raises AdapterValidationError if invalid |
| 191 | +data = adapter.serialize(config) # Returns host-specific dict |
| 192 | +# Result: {"command": "python", "args": ["server.py"], "env": {"DEBUG": "true"}} |
| 193 | +``` |
| 194 | + |
| 195 | +### Backup System Integration |
| 196 | + |
| 197 | +Strategy classes integrate with the backup system via `MCPHostConfigBackupManager`: |
| 198 | + |
| 199 | +```python |
| 200 | +from hatch.mcp_host_config.backup import MCPHostConfigBackupManager, AtomicFileOperations |
| 201 | + |
| 202 | +def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool: |
| 203 | + backup_manager = MCPHostConfigBackupManager() |
| 204 | + atomic_ops = AtomicFileOperations() |
| 205 | + atomic_ops.atomic_write_with_backup( |
| 206 | + file_path=config_path, |
| 207 | + data=existing_data, |
| 208 | + backup_manager=backup_manager, |
| 209 | + hostname="your-host", |
| 210 | + skip_backup=no_backup |
| 211 | + ) |
| 212 | +``` |
| 213 | + |
| 214 | +### Environment Manager Integration |
| 215 | + |
| 216 | +The system integrates with environment management: |
| 217 | + |
| 218 | +- **Single-server-per-package constraint**: One MCP server per installed package |
| 219 | +- **Multi-host configuration**: One server can be configured across multiple hosts |
| 220 | +- **Synchronization support**: Environment data can be synced to available hosts |
| 221 | + |
| 222 | +## Extension Points |
| 223 | + |
| 224 | +### Adding New Host Platforms |
| 225 | + |
| 226 | +To add a new host, complete these steps: |
| 227 | + |
| 228 | +| Step | Files to Modify | |
| 229 | +|------|-----------------| |
| 230 | +| 1. Add host type enum | `models.py` (MCPHostType) | |
| 231 | +| 2. Create adapter class | `adapters/your_host.py` + `adapters/__init__.py` | |
| 232 | +| 3. Create strategy class | `strategies.py` | |
| 233 | +| 4. Add tests | `tests/unit/mcp/`, `tests/integration/mcp/` | |
| 234 | + |
| 235 | +**Minimal adapter implementation:** |
| 236 | + |
| 237 | +```python |
| 238 | +from hatch.mcp_host_config.adapters.base import BaseAdapter |
| 239 | +from hatch.mcp_host_config.fields import UNIVERSAL_FIELDS |
| 240 | + |
| 241 | +class NewHostAdapter(BaseAdapter): |
| 242 | + @property |
| 243 | + def host_name(self) -> str: |
| 244 | + return "new-host" |
| 245 | + |
| 246 | + def get_supported_fields(self) -> FrozenSet[str]: |
| 247 | + return UNIVERSAL_FIELDS | frozenset({"your_specific_field"}) |
| 248 | + |
| 249 | + def validate(self, config: MCPServerConfig) -> None: |
| 250 | + if not config.command and not config.url: |
| 251 | + raise AdapterValidationError("Need command or url") |
| 252 | + |
| 253 | + def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: |
| 254 | + self.validate(config) |
| 255 | + return self.filter_fields(config) |
| 256 | +``` |
| 257 | + |
| 258 | +See [Implementation Guide](../implementation_guides/mcp_host_configuration_extension.md) for complete instructions. |
| 259 | + |
| 260 | +## Design Patterns |
| 261 | + |
| 262 | +### Declarative Field Support |
| 263 | + |
| 264 | +Each adapter declares its supported fields as a `FrozenSet`: |
| 265 | + |
| 266 | +```python |
| 267 | +class YourAdapter(BaseAdapter): |
| 268 | + def get_supported_fields(self) -> FrozenSet[str]: |
| 269 | + return UNIVERSAL_FIELDS | frozenset({"your_field"}) |
| 270 | +``` |
| 271 | + |
| 272 | +The base class provides `filter_fields()` which: |
| 273 | +1. Filters to only supported fields |
| 274 | +2. Removes excluded fields (`name`) |
| 275 | +3. Removes `None` values |
| 276 | + |
| 277 | +### Field Mappings (Optional) |
| 278 | + |
| 279 | +If your host uses different field names: |
| 280 | + |
| 281 | +```python |
| 282 | +CODEX_FIELD_MAPPINGS = { |
| 283 | + "args": "arguments", # Universal → Codex naming |
| 284 | + "headers": "http_headers", # Universal → Codex naming |
| 285 | +} |
| 286 | +``` |
| 287 | + |
| 288 | +### Atomic Operations Pattern |
| 289 | + |
| 290 | +All configuration changes use atomic operations: |
| 291 | + |
| 292 | +1. **Create backup** of current configuration |
| 293 | +2. **Perform modification** to configuration file |
| 294 | +3. **Verify success** and update state |
| 295 | +4. **Clean up** or rollback on failure |
| 296 | + |
| 297 | +## Module Organization |
| 298 | + |
| 299 | +``` |
| 300 | +hatch/mcp_host_config/ |
| 301 | +├── __init__.py # Public API exports |
| 302 | +├── models.py # MCPServerConfig, MCPHostType, HostConfiguration |
| 303 | +├── fields.py # Field constants (UNIVERSAL_FIELDS, etc.) |
| 304 | +├── host_management.py # Registry and configuration manager |
| 305 | +├── strategies.py # Host strategy implementations (I/O) |
| 306 | +├── backup.py # Backup manager and atomic operations |
| 307 | +└── adapters/ |
| 308 | + ├── __init__.py # Adapter exports |
| 309 | + ├── base.py # BaseAdapter abstract class |
| 310 | + ├── registry.py # AdapterRegistry |
| 311 | + ├── claude.py # ClaudeAdapter |
| 312 | + ├── vscode.py # VSCodeAdapter |
| 313 | + ├── cursor.py # CursorAdapter |
| 314 | + ├── gemini.py # GeminiAdapter |
| 315 | + ├── kiro.py # KiroAdapter |
| 316 | + ├── codex.py # CodexAdapter |
| 317 | + └── lmstudio.py # LMStudioAdapter |
| 318 | +``` |
| 319 | + |
| 320 | +## Error Handling |
| 321 | + |
| 322 | +The system uses both exceptions and result objects: |
| 323 | + |
| 324 | +- **Validation errors**: `AdapterValidationError` with field and host context |
| 325 | +- **Configuration operations**: `ConfigurationResult` with success status and messages |
| 326 | + |
| 327 | +```python |
| 328 | +try: |
| 329 | + adapter.validate(config) |
| 330 | +except AdapterValidationError as e: |
| 331 | + print(f"Validation failed: {e.message}") |
| 332 | + print(f"Field: {e.field}, Host: {e.host_name}") |
| 333 | +``` |
| 334 | + |
| 335 | +## Testing Strategy |
| 336 | + |
| 337 | +The test architecture follows a three-tier structure: |
| 338 | + |
| 339 | +| Tier | Location | Purpose | |
| 340 | +|------|----------|---------| |
| 341 | +| Unit | `tests/unit/mcp/` | Adapter protocol, model validation, registry | |
| 342 | +| Integration | `tests/integration/mcp/` | CLI → Adapter → Strategy flow | |
| 343 | +| Regression | `tests/regression/mcp/` | Field filtering edge cases | |
| 344 | + |
| 345 | +See [Test Architecture](../../devs/architecture/test_architecture.md) for details. |
| 346 | + |
0 commit comments