hatch-xclam 0.7.1.dev3__py3-none-any.whl → 0.8.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. hatch/__init__.py +1 -1
  2. hatch/cli/__init__.py +71 -0
  3. hatch/cli/__main__.py +1035 -0
  4. hatch/cli/cli_env.py +865 -0
  5. hatch/cli/cli_mcp.py +1965 -0
  6. hatch/cli/cli_package.py +566 -0
  7. hatch/cli/cli_system.py +136 -0
  8. hatch/cli/cli_utils.py +1289 -0
  9. hatch/cli_hatch.py +160 -2838
  10. hatch/mcp_host_config/__init__.py +10 -10
  11. hatch/mcp_host_config/adapters/__init__.py +34 -0
  12. hatch/mcp_host_config/adapters/base.py +170 -0
  13. hatch/mcp_host_config/adapters/claude.py +105 -0
  14. hatch/mcp_host_config/adapters/codex.py +104 -0
  15. hatch/mcp_host_config/adapters/cursor.py +83 -0
  16. hatch/mcp_host_config/adapters/gemini.py +75 -0
  17. hatch/mcp_host_config/adapters/kiro.py +78 -0
  18. hatch/mcp_host_config/adapters/lmstudio.py +79 -0
  19. hatch/mcp_host_config/adapters/registry.py +149 -0
  20. hatch/mcp_host_config/adapters/vscode.py +83 -0
  21. hatch/mcp_host_config/backup.py +5 -3
  22. hatch/mcp_host_config/fields.py +126 -0
  23. hatch/mcp_host_config/models.py +161 -456
  24. hatch/mcp_host_config/reporting.py +57 -16
  25. hatch/mcp_host_config/strategies.py +155 -87
  26. hatch/template_generator.py +1 -1
  27. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/METADATA +3 -2
  28. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/RECORD +52 -43
  29. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/WHEEL +1 -1
  30. hatch_xclam-0.8.0.dev1.dist-info/entry_points.txt +2 -0
  31. tests/cli_test_utils.py +280 -0
  32. tests/integration/cli/__init__.py +14 -0
  33. tests/integration/cli/test_cli_reporter_integration.py +2439 -0
  34. tests/integration/mcp/__init__.py +0 -0
  35. tests/integration/mcp/test_adapter_serialization.py +173 -0
  36. tests/regression/cli/__init__.py +16 -0
  37. tests/regression/cli/test_color_logic.py +268 -0
  38. tests/regression/cli/test_consequence_type.py +298 -0
  39. tests/regression/cli/test_error_formatting.py +328 -0
  40. tests/regression/cli/test_result_reporter.py +586 -0
  41. tests/regression/cli/test_table_formatter.py +211 -0
  42. tests/regression/mcp/__init__.py +0 -0
  43. tests/regression/mcp/test_field_filtering.py +162 -0
  44. tests/test_cli_version.py +7 -5
  45. tests/test_data/fixtures/cli_reporter_fixtures.py +184 -0
  46. tests/unit/__init__.py +0 -0
  47. tests/unit/mcp/__init__.py +0 -0
  48. tests/unit/mcp/test_adapter_protocol.py +138 -0
  49. tests/unit/mcp/test_adapter_registry.py +158 -0
  50. tests/unit/mcp/test_config_model.py +146 -0
  51. hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt +0 -2
  52. tests/integration/test_mcp_kiro_integration.py +0 -153
  53. tests/regression/test_mcp_codex_backup_integration.py +0 -162
  54. tests/regression/test_mcp_codex_host_strategy.py +0 -163
  55. tests/regression/test_mcp_codex_model_validation.py +0 -117
  56. tests/regression/test_mcp_kiro_backup_integration.py +0 -241
  57. tests/regression/test_mcp_kiro_cli_integration.py +0 -141
  58. tests/regression/test_mcp_kiro_decorator_registration.py +0 -71
  59. tests/regression/test_mcp_kiro_host_strategy.py +0 -214
  60. tests/regression/test_mcp_kiro_model_validation.py +0 -116
  61. tests/regression/test_mcp_kiro_omni_conversion.py +0 -104
  62. tests/test_mcp_atomic_operations.py +0 -276
  63. tests/test_mcp_backup_integration.py +0 -308
  64. tests/test_mcp_cli_all_host_specific_args.py +0 -496
  65. tests/test_mcp_cli_backup_management.py +0 -295
  66. tests/test_mcp_cli_direct_management.py +0 -456
  67. tests/test_mcp_cli_discovery_listing.py +0 -582
  68. tests/test_mcp_cli_host_config_integration.py +0 -823
  69. tests/test_mcp_cli_package_management.py +0 -360
  70. tests/test_mcp_cli_partial_updates.py +0 -859
  71. tests/test_mcp_environment_integration.py +0 -520
  72. tests/test_mcp_host_config_backup.py +0 -257
  73. tests/test_mcp_host_configuration_manager.py +0 -331
  74. tests/test_mcp_host_registry_decorator.py +0 -348
  75. tests/test_mcp_pydantic_architecture_v4.py +0 -603
  76. tests/test_mcp_server_config_models.py +0 -242
  77. tests/test_mcp_server_config_type_field.py +0 -221
  78. tests/test_mcp_sync_functionality.py +0 -316
  79. tests/test_mcp_user_feedback_reporting.py +0 -359
  80. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/licenses/LICENSE +0 -0
  81. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,78 @@
1
+ """Kiro adapter for MCP host configuration.
2
+
3
+ Kiro has specific features:
4
+ - No 'type' field support
5
+ - Server enable/disable via 'disabled' field
6
+ - Tool management: autoApprove, disabledTools
7
+ """
8
+
9
+ from typing import Any, Dict, FrozenSet
10
+
11
+ from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter
12
+ from hatch.mcp_host_config.fields import KIRO_FIELDS
13
+ from hatch.mcp_host_config.models import MCPServerConfig
14
+
15
+
16
+ class KiroAdapter(BaseAdapter):
17
+ """Adapter for Kiro MCP host.
18
+
19
+ Kiro is similar to Claude but without the 'type' field:
20
+ - Requires exactly one transport (command XOR url)
21
+ - Has 'disabled' field for toggling server
22
+ - Has 'autoApprove' for auto-approved tools
23
+ - Has 'disabledTools' for disabled tools
24
+ """
25
+
26
+ @property
27
+ def host_name(self) -> str:
28
+ """Return the host identifier."""
29
+ return "kiro"
30
+
31
+ def get_supported_fields(self) -> FrozenSet[str]:
32
+ """Return fields supported by Kiro."""
33
+ return KIRO_FIELDS
34
+
35
+ def validate(self, config: MCPServerConfig) -> None:
36
+ """Validate configuration for Kiro.
37
+
38
+ Like Claude, requires exactly one transport.
39
+ Does not support 'type' field.
40
+ """
41
+ has_command = config.command is not None
42
+ has_url = config.url is not None
43
+ has_http_url = config.httpUrl is not None
44
+
45
+ # Kiro doesn't support httpUrl
46
+ if has_http_url:
47
+ raise AdapterValidationError(
48
+ "httpUrl is not supported (use 'url' for remote servers)",
49
+ field="httpUrl",
50
+ host_name=self.host_name
51
+ )
52
+
53
+ # Must have exactly one transport
54
+ if not has_command and not has_url:
55
+ raise AdapterValidationError(
56
+ "Either 'command' (local) or 'url' (remote) must be specified",
57
+ host_name=self.host_name
58
+ )
59
+
60
+ if has_command and has_url:
61
+ raise AdapterValidationError(
62
+ "Cannot specify both 'command' and 'url' - choose one transport",
63
+ host_name=self.host_name
64
+ )
65
+
66
+ # 'type' field is not supported by Kiro
67
+ if config.type is not None:
68
+ raise AdapterValidationError(
69
+ "'type' field is not supported by Kiro",
70
+ field="type",
71
+ host_name=self.host_name
72
+ )
73
+
74
+ def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
75
+ """Serialize configuration for Kiro format."""
76
+ self.validate(config)
77
+ return self.filter_fields(config)
78
+
@@ -0,0 +1,79 @@
1
+ """LM Studio adapter for MCP host configuration.
2
+
3
+ LM Studio follows the Cursor/Claude format with the same field set.
4
+ """
5
+
6
+ from typing import Any, Dict, FrozenSet
7
+
8
+ from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter
9
+ from hatch.mcp_host_config.fields import LMSTUDIO_FIELDS
10
+ from hatch.mcp_host_config.models import MCPServerConfig
11
+
12
+
13
+ class LMStudioAdapter(BaseAdapter):
14
+ """Adapter for LM Studio MCP host.
15
+
16
+ LM Studio uses the same configuration format as Claude/Cursor:
17
+ - Supports 'type' field for transport discrimination
18
+ - Requires exactly one transport (command XOR url)
19
+ """
20
+
21
+ @property
22
+ def host_name(self) -> str:
23
+ """Return the host identifier."""
24
+ return "lmstudio"
25
+
26
+ def get_supported_fields(self) -> FrozenSet[str]:
27
+ """Return fields supported by LM Studio."""
28
+ return LMSTUDIO_FIELDS
29
+
30
+ def validate(self, config: MCPServerConfig) -> None:
31
+ """Validate configuration for LM Studio.
32
+
33
+ Same rules as Claude: exactly one transport required.
34
+ """
35
+ has_command = config.command is not None
36
+ has_url = config.url is not None
37
+ has_http_url = config.httpUrl is not None
38
+
39
+ # LM Studio doesn't support httpUrl
40
+ if has_http_url:
41
+ raise AdapterValidationError(
42
+ "httpUrl is not supported (use 'url' for remote servers)",
43
+ field="httpUrl",
44
+ host_name=self.host_name
45
+ )
46
+
47
+ # Must have exactly one transport
48
+ if not has_command and not has_url:
49
+ raise AdapterValidationError(
50
+ "Either 'command' (local) or 'url' (remote) must be specified",
51
+ host_name=self.host_name
52
+ )
53
+
54
+ if has_command and has_url:
55
+ raise AdapterValidationError(
56
+ "Cannot specify both 'command' and 'url' - choose one transport",
57
+ host_name=self.host_name
58
+ )
59
+
60
+ # Validate type consistency if specified
61
+ if config.type is not None:
62
+ if config.type == "stdio" and not has_command:
63
+ raise AdapterValidationError(
64
+ "type='stdio' requires 'command' field",
65
+ field="type",
66
+ host_name=self.host_name
67
+ )
68
+ if config.type in ("sse", "http") and not has_url:
69
+ raise AdapterValidationError(
70
+ f"type='{config.type}' requires 'url' field",
71
+ field="type",
72
+ host_name=self.host_name
73
+ )
74
+
75
+ def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
76
+ """Serialize configuration for LM Studio format."""
77
+ self.validate(config)
78
+ return self.filter_fields(config)
79
+
@@ -0,0 +1,149 @@
1
+ """Adapter registry for MCP host configurations.
2
+
3
+ This module provides a centralized registry for host-specific adapters.
4
+ The registry maps host names to adapter instances and provides factory methods.
5
+ """
6
+
7
+ from typing import Dict, List, Optional, Type
8
+
9
+ from hatch.mcp_host_config.adapters.base import BaseAdapter
10
+ from hatch.mcp_host_config.adapters.claude import ClaudeAdapter
11
+ from hatch.mcp_host_config.adapters.codex import CodexAdapter
12
+ from hatch.mcp_host_config.adapters.cursor import CursorAdapter
13
+ from hatch.mcp_host_config.adapters.gemini import GeminiAdapter
14
+ from hatch.mcp_host_config.adapters.kiro import KiroAdapter
15
+ from hatch.mcp_host_config.adapters.lmstudio import LMStudioAdapter
16
+ from hatch.mcp_host_config.adapters.vscode import VSCodeAdapter
17
+
18
+
19
+ class AdapterRegistry:
20
+ """Registry for MCP host configuration adapters.
21
+
22
+ The registry provides:
23
+ - Host name to adapter mapping
24
+ - Factory method to get adapters by host name
25
+ - Registration of custom adapters
26
+ - List of all supported hosts
27
+
28
+ Example:
29
+ >>> registry = AdapterRegistry()
30
+ >>> adapter = registry.get_adapter("claude-desktop")
31
+ >>> adapter.host_name
32
+ 'claude-desktop'
33
+
34
+ >>> registry.get_supported_hosts()
35
+ ['claude-code', 'claude-desktop', 'codex', 'cursor', 'gemini', 'kiro', 'lmstudio', 'vscode']
36
+ """
37
+
38
+ def __init__(self):
39
+ """Initialize the registry with default adapters."""
40
+ self._adapters: Dict[str, BaseAdapter] = {}
41
+ self._register_defaults()
42
+
43
+ def _register_defaults(self) -> None:
44
+ """Register all built-in adapters."""
45
+ # Claude variants
46
+ self.register(ClaudeAdapter(variant="desktop"))
47
+ self.register(ClaudeAdapter(variant="code"))
48
+
49
+ # Other hosts
50
+ self.register(VSCodeAdapter())
51
+ self.register(CursorAdapter())
52
+ self.register(LMStudioAdapter())
53
+ self.register(GeminiAdapter())
54
+ self.register(KiroAdapter())
55
+ self.register(CodexAdapter())
56
+
57
+ def register(self, adapter: BaseAdapter) -> None:
58
+ """Register an adapter instance.
59
+
60
+ Args:
61
+ adapter: The adapter instance to register
62
+
63
+ Raises:
64
+ ValueError: If an adapter with the same host name is already registered
65
+ """
66
+ host_name = adapter.host_name
67
+ if host_name in self._adapters:
68
+ raise ValueError(f"Adapter for '{host_name}' is already registered")
69
+ self._adapters[host_name] = adapter
70
+
71
+ def get_adapter(self, host_name: str) -> BaseAdapter:
72
+ """Get an adapter by host name.
73
+
74
+ Args:
75
+ host_name: The host identifier (e.g., "claude-desktop", "gemini")
76
+
77
+ Returns:
78
+ The adapter instance for the specified host
79
+
80
+ Raises:
81
+ KeyError: If no adapter is registered for the host name
82
+ """
83
+ if host_name not in self._adapters:
84
+ supported = ", ".join(sorted(self._adapters.keys()))
85
+ raise KeyError(f"No adapter registered for '{host_name}'. Supported hosts: {supported}")
86
+ return self._adapters[host_name]
87
+
88
+ def has_adapter(self, host_name: str) -> bool:
89
+ """Check if an adapter is registered for a host name.
90
+
91
+ Args:
92
+ host_name: The host identifier to check
93
+
94
+ Returns:
95
+ True if an adapter is registered, False otherwise
96
+ """
97
+ return host_name in self._adapters
98
+
99
+ def get_supported_hosts(self) -> List[str]:
100
+ """Get a sorted list of all supported host names.
101
+
102
+ Returns:
103
+ Sorted list of host name strings
104
+ """
105
+ return sorted(self._adapters.keys())
106
+
107
+ def unregister(self, host_name: str) -> None:
108
+ """Unregister an adapter by host name.
109
+
110
+ Args:
111
+ host_name: The host identifier to unregister
112
+
113
+ Raises:
114
+ KeyError: If no adapter is registered for the host name
115
+ """
116
+ if host_name not in self._adapters:
117
+ raise KeyError(f"No adapter registered for '{host_name}'")
118
+ del self._adapters[host_name]
119
+
120
+
121
+ # Global registry instance for convenience
122
+ _default_registry: Optional[AdapterRegistry] = None
123
+
124
+
125
+ def get_default_registry() -> AdapterRegistry:
126
+ """Get the default global adapter registry.
127
+
128
+ Returns:
129
+ The singleton AdapterRegistry instance
130
+ """
131
+ global _default_registry
132
+ if _default_registry is None:
133
+ _default_registry = AdapterRegistry()
134
+ return _default_registry
135
+
136
+
137
+ def get_adapter(host_name: str) -> BaseAdapter:
138
+ """Get an adapter from the default registry.
139
+
140
+ This is a convenience function that uses the global registry.
141
+
142
+ Args:
143
+ host_name: The host identifier (e.g., "claude-desktop", "gemini")
144
+
145
+ Returns:
146
+ The adapter instance for the specified host
147
+ """
148
+ return get_default_registry().get_adapter(host_name)
149
+
@@ -0,0 +1,83 @@
1
+ """VSCode adapter for MCP host configuration.
2
+
3
+ VSCode extends Claude's format with:
4
+ - envFile: Path to environment file
5
+ - inputs: Input variable definitions (VSCode only)
6
+ """
7
+
8
+ from typing import Any, Dict, FrozenSet
9
+
10
+ from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter
11
+ from hatch.mcp_host_config.fields import VSCODE_FIELDS
12
+ from hatch.mcp_host_config.models import MCPServerConfig
13
+
14
+
15
+ class VSCodeAdapter(BaseAdapter):
16
+ """Adapter for Visual Studio Code MCP host.
17
+
18
+ VSCode supports the same base configuration as Claude, plus:
19
+ - envFile: Path to a .env file for environment variables
20
+ - inputs: Array of input variable definitions for prompts
21
+
22
+ Like Claude, it requires exactly one transport (command XOR url).
23
+ """
24
+
25
+ @property
26
+ def host_name(self) -> str:
27
+ """Return the host identifier."""
28
+ return "vscode"
29
+
30
+ def get_supported_fields(self) -> FrozenSet[str]:
31
+ """Return fields supported by VSCode."""
32
+ return VSCODE_FIELDS
33
+
34
+ def validate(self, config: MCPServerConfig) -> None:
35
+ """Validate configuration for VSCode.
36
+
37
+ Same rules as Claude: exactly one transport required.
38
+ """
39
+ has_command = config.command is not None
40
+ has_url = config.url is not None
41
+ has_http_url = config.httpUrl is not None
42
+
43
+ # VSCode doesn't support httpUrl
44
+ if has_http_url:
45
+ raise AdapterValidationError(
46
+ "httpUrl is not supported (use 'url' for remote servers)",
47
+ field="httpUrl",
48
+ host_name=self.host_name
49
+ )
50
+
51
+ # Must have exactly one transport
52
+ if not has_command and not has_url:
53
+ raise AdapterValidationError(
54
+ "Either 'command' (local) or 'url' (remote) must be specified",
55
+ host_name=self.host_name
56
+ )
57
+
58
+ if has_command and has_url:
59
+ raise AdapterValidationError(
60
+ "Cannot specify both 'command' and 'url' - choose one transport",
61
+ host_name=self.host_name
62
+ )
63
+
64
+ # Validate type consistency if specified
65
+ if config.type is not None:
66
+ if config.type == "stdio" and not has_command:
67
+ raise AdapterValidationError(
68
+ "type='stdio' requires 'command' field",
69
+ field="type",
70
+ host_name=self.host_name
71
+ )
72
+ if config.type in ("sse", "http") and not has_url:
73
+ raise AdapterValidationError(
74
+ f"type='{config.type}' requires 'url' field",
75
+ field="type",
76
+ host_name=self.host_name
77
+ )
78
+
79
+ def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
80
+ """Serialize configuration for VSCode format."""
81
+ self.validate(config)
82
+ return self.filter_fields(config)
83
+
@@ -355,10 +355,12 @@ class MCPHostConfigBackupManager:
355
355
 
356
356
  backups = []
357
357
 
358
- # Search for both correct format and legacy incorrect format for backward compatibility
358
+ # Search for backups with flexible filename matching
359
+ # Different hosts use different config filenames (mcp.json, settings.json, config.toml)
360
+ # Backup format: {original_filename}.{hostname}.{timestamp}
359
361
  patterns = [
360
- f"mcp.json.{hostname}.*", # Correct format: mcp.json.gemini.*
361
- f"mcp.json.MCPHostType.{hostname.upper()}.*" # Legacy incorrect format: mcp.json.MCPHostType.GEMINI.*
362
+ f"*.{hostname}.*", # Flexible: settings.json.gemini.*, mcp.json.claude-desktop.*, etc.
363
+ f"mcp.json.MCPHostType.{hostname.upper()}.*" # Legacy incorrect format for backward compatibility
362
364
  ]
363
365
 
364
366
  for pattern in patterns:
@@ -0,0 +1,126 @@
1
+ """
2
+ Field constants for MCP host configuration adapter architecture.
3
+
4
+ This module defines the source of truth for field support across MCP hosts.
5
+ All adapters reference these constants to determine field filtering and mapping.
6
+ """
7
+
8
+ from typing import FrozenSet
9
+ from enum import Enum
10
+
11
+
12
+ # ============================================================================
13
+ # Universal Fields (supported by ALL hosts)
14
+ # ============================================================================
15
+
16
+ UNIVERSAL_FIELDS: FrozenSet[str] = frozenset({
17
+ "command", # Executable path/name for local servers
18
+ "args", # Command arguments for local servers
19
+ "env", # Environment variables (all transports)
20
+ "url", # Server endpoint URL for remote servers (SSE transport)
21
+ "headers", # HTTP headers for remote servers
22
+ })
23
+
24
+
25
+ # ============================================================================
26
+ # Type Field Support
27
+ # ============================================================================
28
+
29
+ # Hosts that support the 'type' discriminator field (stdio/sse/http)
30
+ # Note: Gemini, Kiro, Codex do NOT support this field
31
+ TYPE_SUPPORTING_HOSTS: FrozenSet[str] = frozenset({
32
+ "claude-desktop",
33
+ "claude-code",
34
+ "vscode",
35
+ "cursor",
36
+ })
37
+
38
+
39
+ # ============================================================================
40
+ # Host-Specific Field Sets
41
+ # ============================================================================
42
+
43
+ # Fields supported by Claude Desktop/Code (universal + type)
44
+ CLAUDE_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset({
45
+ "type", # Transport discriminator
46
+ })
47
+
48
+ # Fields supported by VSCode (Claude fields + envFile + inputs)
49
+ VSCODE_FIELDS: FrozenSet[str] = CLAUDE_FIELDS | frozenset({
50
+ "envFile", # Path to environment file
51
+ "inputs", # Input variable definitions (VSCode only)
52
+ })
53
+
54
+ # Fields supported by Cursor (Claude fields + envFile, no inputs)
55
+ CURSOR_FIELDS: FrozenSet[str] = CLAUDE_FIELDS | frozenset({
56
+ "envFile", # Path to environment file
57
+ })
58
+
59
+ # Fields supported by LMStudio (universal + type)
60
+ LMSTUDIO_FIELDS: FrozenSet[str] = CLAUDE_FIELDS
61
+
62
+ # Fields supported by Gemini (no type field, but has httpUrl and others)
63
+ GEMINI_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset({
64
+ "httpUrl", # HTTP streaming endpoint URL
65
+ "timeout", # Request timeout in milliseconds
66
+ "trust", # Bypass tool call confirmations
67
+ "cwd", # Working directory for stdio transport
68
+ "includeTools", # Tools to include (allowlist)
69
+ "excludeTools", # Tools to exclude (blocklist)
70
+ # OAuth configuration
71
+ "oauth_enabled",
72
+ "oauth_clientId",
73
+ "oauth_clientSecret",
74
+ "oauth_authorizationUrl",
75
+ "oauth_tokenUrl",
76
+ "oauth_scopes",
77
+ "oauth_redirectUri",
78
+ "oauth_tokenParamName",
79
+ "oauth_audiences",
80
+ "authProviderType",
81
+ })
82
+
83
+ # Fields supported by Kiro (no type field)
84
+ KIRO_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset({
85
+ "disabled", # Whether server is disabled
86
+ "autoApprove", # Auto-approved tool names
87
+ "disabledTools", # Disabled tool names
88
+ })
89
+
90
+ # Fields supported by Codex (no type field, has field mappings)
91
+ CODEX_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset({
92
+ "cwd", # Working directory
93
+ "env_vars", # Environment variables to whitelist/forward
94
+ "startup_timeout_sec", # Server startup timeout
95
+ "tool_timeout_sec", # Tool execution timeout
96
+ "enabled", # Enable/disable server
97
+ "enabled_tools", # Allow-list of tools
98
+ "disabled_tools", # Deny-list of tools
99
+ "bearer_token_env_var",# Env var containing bearer token
100
+ "http_headers", # HTTP headers (Codex naming)
101
+ "env_http_headers", # Header names to env var names mapping
102
+ })
103
+
104
+
105
+ # ============================================================================
106
+ # Field Mappings (universal name → host-specific name)
107
+ # ============================================================================
108
+
109
+ # Codex uses different field names for some universal/shared fields
110
+ CODEX_FIELD_MAPPINGS: dict[str, str] = {
111
+ "args": "arguments", # Codex uses 'arguments' instead of 'args'
112
+ "headers": "http_headers", # Codex uses 'http_headers' instead of 'headers'
113
+ "includeTools": "enabled_tools", # Gemini naming → Codex naming
114
+ "excludeTools": "disabled_tools", # Gemini naming → Codex naming
115
+ }
116
+
117
+
118
+ # ============================================================================
119
+ # Metadata Fields (never serialized to host config files)
120
+ # ============================================================================
121
+
122
+ # Fields that are Hatch metadata and should NEVER appear in serialized output
123
+ EXCLUDED_ALWAYS: FrozenSet[str] = frozenset({
124
+ "name", # Server name is key in the config dict, not a field value
125
+ })
126
+