Skip to content

Commit 061ae53

Browse files
LittleCoinCoinLittleCoinCoin
authored andcommitted
feat(codex): add MCPServerConfigCodex model and infrastructure
Add complete Codex MCP host support infrastructure: - Add MCPHostType.CODEX enum value - Add 'codex' to backup hostname validation - Create MCPServerConfigCodex model with 10 Codex-specific fields: * env_vars: Environment variable whitelist * cwd: Working directory * startup_timeout_sec, tool_timeout_sec: Timeout controls * enabled: Server enable/disable flag * enabled_tools, disabled_tools: Tool filtering * bearer_token_env_var: Bearer token environment variable * http_headers, env_http_headers: HTTP authentication - Extend MCPServerConfigOmni with all Codex fields - Update HOST_MODEL_REGISTRY to map CODEX to model - Export MCPServerConfigCodex from module - Add atomic_write_with_serializer() method for format-agnostic writes - Refactor atomic_write_with_backup() to use new serializer method This enables TOML configuration support while maintaining backward compatibility with existing JSON-based hosts.
1 parent 00b960f commit 061ae53

File tree

3 files changed

+143
-36
lines changed

3 files changed

+143
-36
lines changed

hatch/mcp_host_config/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
PackageHostConfiguration, EnvironmentPackageEntry, ConfigurationResult, SyncResult,
1212
# Host-specific configuration models
1313
MCPServerConfigBase, MCPServerConfigGemini, MCPServerConfigVSCode,
14-
MCPServerConfigCursor, MCPServerConfigClaude, MCPServerConfigKiro, MCPServerConfigOmni,
14+
MCPServerConfigCursor, MCPServerConfigClaude, MCPServerConfigKiro,
15+
MCPServerConfigCodex, MCPServerConfigOmni,
1516
HOST_MODEL_REGISTRY
1617
)
1718
from .host_management import (
@@ -30,7 +31,8 @@
3031
'PackageHostConfiguration', 'EnvironmentPackageEntry', 'ConfigurationResult', 'SyncResult',
3132
# Host-specific configuration models
3233
'MCPServerConfigBase', 'MCPServerConfigGemini', 'MCPServerConfigVSCode',
33-
'MCPServerConfigCursor', 'MCPServerConfigClaude', 'MCPServerConfigKiro', 'MCPServerConfigOmni',
34+
'MCPServerConfigCursor', 'MCPServerConfigClaude', 'MCPServerConfigKiro',
35+
'MCPServerConfigCodex', 'MCPServerConfigOmni',
3436
'HOST_MODEL_REGISTRY',
3537
# User feedback reporting
3638
'FieldOperation', 'ConversionReport', 'generate_conversion_report', 'display_report',

hatch/mcp_host_config/backup.py

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import tempfile
1010
from datetime import datetime
1111
from pathlib import Path
12-
from typing import Dict, List, Optional, Any
12+
from typing import Dict, List, Optional, Any, Callable, TextIO
1313

1414
from pydantic import BaseModel, Field, validator
1515

@@ -36,8 +36,8 @@ class BackupInfo(BaseModel):
3636
def validate_hostname(cls, v):
3737
"""Validate hostname is supported."""
3838
supported_hosts = {
39-
'claude-desktop', 'claude-code', 'vscode',
40-
'cursor', 'lmstudio', 'gemini', 'kiro'
39+
'claude-desktop', 'claude-code', 'vscode',
40+
'cursor', 'lmstudio', 'gemini', 'kiro', 'codex'
4141
}
4242
if v not in supported_hosts:
4343
raise ValueError(f"Unsupported hostname: {v}. Supported: {supported_hosts}")
@@ -101,22 +101,29 @@ class Config:
101101

102102
class AtomicFileOperations:
103103
"""Atomic file operations for safe configuration updates."""
104-
105-
def atomic_write_with_backup(self, file_path: Path, data: Dict[str, Any],
106-
backup_manager: "MCPHostConfigBackupManager",
107-
hostname: str, skip_backup: bool = False) -> bool:
108-
"""Atomic write with automatic backup creation.
109-
104+
105+
def atomic_write_with_serializer(
106+
self,
107+
file_path: Path,
108+
data: Any,
109+
serializer: Callable[[Any, TextIO], None],
110+
backup_manager: "MCPHostConfigBackupManager",
111+
hostname: str,
112+
skip_backup: bool = False
113+
) -> bool:
114+
"""Atomic write with custom serializer and automatic backup creation.
115+
110116
Args:
111-
file_path (Path): Target file path for writing
112-
data (Dict[str, Any]): Data to write as JSON
113-
backup_manager (MCPHostConfigBackupManager): Backup manager instance
114-
hostname (str): Host identifier for backup
115-
skip_backup (bool, optional): Skip backup creation. Defaults to False.
116-
117+
file_path: Target file path for writing
118+
data: Data to serialize and write
119+
serializer: Function that writes data to file handle
120+
backup_manager: Backup manager instance
121+
hostname: Host identifier for backup
122+
skip_backup: Skip backup creation
123+
117124
Returns:
118-
bool: True if operation successful, False otherwise
119-
125+
bool: True if operation successful
126+
120127
Raises:
121128
BackupError: If backup creation fails and skip_backup is False
122129
"""
@@ -126,32 +133,52 @@ def atomic_write_with_backup(self, file_path: Path, data: Dict[str, Any],
126133
backup_result = backup_manager.create_backup(file_path, hostname)
127134
if not backup_result.success:
128135
raise BackupError(f"Required backup failed: {backup_result.error_message}")
129-
130-
# Create temporary file for atomic write
136+
131137
temp_file = None
132138
try:
133-
# Write to temporary file first
134139
temp_file = file_path.with_suffix(f"{file_path.suffix}.tmp")
135140
with open(temp_file, 'w', encoding='utf-8') as f:
136-
json.dump(data, f, indent=2, ensure_ascii=False)
137-
138-
# Atomic move to target location
141+
serializer(data, f)
142+
139143
temp_file.replace(file_path)
140144
return True
141-
145+
142146
except Exception as e:
143-
# Clean up temporary file on failure
144147
if temp_file and temp_file.exists():
145148
temp_file.unlink()
146-
147-
# Restore from backup if available
149+
148150
if backup_result and backup_result.backup_path:
149151
try:
150152
backup_manager.restore_backup(hostname, backup_result.backup_path.name)
151153
except Exception:
152-
pass # Log but don't raise - original error is more important
153-
154+
pass
155+
154156
raise BackupError(f"Atomic write failed: {str(e)}")
157+
158+
def atomic_write_with_backup(self, file_path: Path, data: Dict[str, Any],
159+
backup_manager: "MCPHostConfigBackupManager",
160+
hostname: str, skip_backup: bool = False) -> bool:
161+
"""Atomic write with JSON serialization (backward compatible).
162+
163+
Args:
164+
file_path (Path): Target file path for writing
165+
data (Dict[str, Any]): Data to write as JSON
166+
backup_manager (MCPHostConfigBackupManager): Backup manager instance
167+
hostname (str): Host identifier for backup
168+
skip_backup (bool, optional): Skip backup creation. Defaults to False.
169+
170+
Returns:
171+
bool: True if operation successful, False otherwise
172+
173+
Raises:
174+
BackupError: If backup creation fails and skip_backup is False
175+
"""
176+
def json_serializer(data: Any, f: TextIO) -> None:
177+
json.dump(data, f, indent=2, ensure_ascii=False)
178+
179+
return self.atomic_write_with_serializer(
180+
file_path, data, json_serializer, backup_manager, hostname, skip_backup
181+
)
155182

156183
def atomic_copy(self, source: Path, target: Path) -> bool:
157184
"""Atomic file copy operation.

hatch/mcp_host_config/models.py

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class MCPHostType(str, Enum):
2525
LMSTUDIO = "lmstudio"
2626
GEMINI = "gemini"
2727
KIRO = "kiro"
28+
CODEX = "codex"
2829

2930

3031
class MCPServerConfig(BaseModel):
@@ -541,28 +542,93 @@ def from_omni(cls, omni: 'MCPServerConfigOmni') -> 'MCPServerConfigClaude':
541542

542543
class MCPServerConfigKiro(MCPServerConfigBase):
543544
"""Kiro IDE-specific MCP server configuration.
544-
545+
545546
Extends base model with Kiro-specific fields for server management
546547
and tool control.
547548
"""
548-
549+
549550
# Kiro-specific fields
550551
disabled: Optional[bool] = Field(None, description="Whether server is disabled")
551552
autoApprove: Optional[List[str]] = Field(None, description="Auto-approved tool names")
552553
disabledTools: Optional[List[str]] = Field(None, description="Disabled tool names")
553-
554+
554555
@classmethod
555556
def from_omni(cls, omni: 'MCPServerConfigOmni') -> 'MCPServerConfigKiro':
556557
"""Convert Omni model to Kiro-specific model."""
557558
# Get supported fields dynamically
558559
supported_fields = set(cls.model_fields.keys())
559-
560+
560561
# Single-call field filtering
561562
kiro_data = omni.model_dump(include=supported_fields, exclude_unset=True)
562-
563+
563564
return cls.model_validate(kiro_data)
564565

565566

567+
class MCPServerConfigCodex(MCPServerConfigBase):
568+
"""Codex-specific MCP server configuration.
569+
570+
Extends base model with Codex-specific fields including timeouts,
571+
tool filtering, environment variable forwarding, and HTTP authentication.
572+
"""
573+
574+
model_config = ConfigDict(extra="forbid")
575+
576+
# Codex-specific STDIO fields
577+
env_vars: Optional[List[str]] = Field(
578+
None,
579+
description="Environment variables to whitelist/forward"
580+
)
581+
cwd: Optional[str] = Field(
582+
None,
583+
description="Working directory to launch server from"
584+
)
585+
586+
# Timeout configuration
587+
startup_timeout_sec: Optional[int] = Field(
588+
None,
589+
description="Server startup timeout in seconds (default: 10)"
590+
)
591+
tool_timeout_sec: Optional[int] = Field(
592+
None,
593+
description="Tool execution timeout in seconds (default: 60)"
594+
)
595+
596+
# Server control
597+
enabled: Optional[bool] = Field(
598+
None,
599+
description="Enable/disable server without deleting config"
600+
)
601+
enabled_tools: Optional[List[str]] = Field(
602+
None,
603+
description="Allow-list of tools to expose from server"
604+
)
605+
disabled_tools: Optional[List[str]] = Field(
606+
None,
607+
description="Deny-list of tools to hide (applied after enabled_tools)"
608+
)
609+
610+
# HTTP authentication fields
611+
bearer_token_env_var: Optional[str] = Field(
612+
None,
613+
description="Name of env var containing bearer token for Authorization header"
614+
)
615+
http_headers: Optional[Dict[str, str]] = Field(
616+
None,
617+
description="Map of header names to static values"
618+
)
619+
env_http_headers: Optional[Dict[str, str]] = Field(
620+
None,
621+
description="Map of header names to env var names (values pulled from env)"
622+
)
623+
624+
@classmethod
625+
def from_omni(cls, omni: 'MCPServerConfigOmni') -> 'MCPServerConfigCodex':
626+
"""Convert Omni model to Codex-specific model."""
627+
supported_fields = set(cls.model_fields.keys())
628+
codex_data = omni.model_dump(include=supported_fields, exclude_unset=True)
629+
return cls.model_validate(codex_data)
630+
631+
566632
class MCPServerConfigOmni(BaseModel):
567633
"""Omni configuration supporting all host-specific fields.
568634
@@ -611,6 +677,17 @@ class MCPServerConfigOmni(BaseModel):
611677
autoApprove: Optional[List[str]] = None
612678
disabledTools: Optional[List[str]] = None
613679

680+
# Codex specific
681+
env_vars: Optional[List[str]] = None
682+
startup_timeout_sec: Optional[int] = None
683+
tool_timeout_sec: Optional[int] = None
684+
enabled: Optional[bool] = None
685+
enabled_tools: Optional[List[str]] = None
686+
disabled_tools: Optional[List[str]] = None
687+
bearer_token_env_var: Optional[str] = None
688+
http_headers: Optional[Dict[str, str]] = None
689+
env_http_headers: Optional[Dict[str, str]] = None
690+
614691
@field_validator('url')
615692
@classmethod
616693
def validate_url_format(cls, v):
@@ -630,4 +707,5 @@ def validate_url_format(cls, v):
630707
MCPHostType.CURSOR: MCPServerConfigCursor,
631708
MCPHostType.LMSTUDIO: MCPServerConfigCursor, # Same as CURSOR
632709
MCPHostType.KIRO: MCPServerConfigKiro,
710+
MCPHostType.CODEX: MCPServerConfigCodex,
633711
}

0 commit comments

Comments
 (0)