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.
- hatch/__init__.py +1 -1
- hatch/cli/__init__.py +71 -0
- hatch/cli/__main__.py +1035 -0
- hatch/cli/cli_env.py +865 -0
- hatch/cli/cli_mcp.py +1965 -0
- hatch/cli/cli_package.py +566 -0
- hatch/cli/cli_system.py +136 -0
- hatch/cli/cli_utils.py +1289 -0
- hatch/cli_hatch.py +160 -2838
- hatch/mcp_host_config/__init__.py +10 -10
- hatch/mcp_host_config/adapters/__init__.py +34 -0
- hatch/mcp_host_config/adapters/base.py +170 -0
- hatch/mcp_host_config/adapters/claude.py +105 -0
- hatch/mcp_host_config/adapters/codex.py +104 -0
- hatch/mcp_host_config/adapters/cursor.py +83 -0
- hatch/mcp_host_config/adapters/gemini.py +75 -0
- hatch/mcp_host_config/adapters/kiro.py +78 -0
- hatch/mcp_host_config/adapters/lmstudio.py +79 -0
- hatch/mcp_host_config/adapters/registry.py +149 -0
- hatch/mcp_host_config/adapters/vscode.py +83 -0
- hatch/mcp_host_config/backup.py +5 -3
- hatch/mcp_host_config/fields.py +126 -0
- hatch/mcp_host_config/models.py +161 -456
- hatch/mcp_host_config/reporting.py +57 -16
- hatch/mcp_host_config/strategies.py +155 -87
- hatch/template_generator.py +1 -1
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/METADATA +3 -2
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/RECORD +52 -43
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/WHEEL +1 -1
- hatch_xclam-0.8.0.dev1.dist-info/entry_points.txt +2 -0
- tests/cli_test_utils.py +280 -0
- tests/integration/cli/__init__.py +14 -0
- tests/integration/cli/test_cli_reporter_integration.py +2439 -0
- tests/integration/mcp/__init__.py +0 -0
- tests/integration/mcp/test_adapter_serialization.py +173 -0
- tests/regression/cli/__init__.py +16 -0
- tests/regression/cli/test_color_logic.py +268 -0
- tests/regression/cli/test_consequence_type.py +298 -0
- tests/regression/cli/test_error_formatting.py +328 -0
- tests/regression/cli/test_result_reporter.py +586 -0
- tests/regression/cli/test_table_formatter.py +211 -0
- tests/regression/mcp/__init__.py +0 -0
- tests/regression/mcp/test_field_filtering.py +162 -0
- tests/test_cli_version.py +7 -5
- tests/test_data/fixtures/cli_reporter_fixtures.py +184 -0
- tests/unit/__init__.py +0 -0
- tests/unit/mcp/__init__.py +0 -0
- tests/unit/mcp/test_adapter_protocol.py +138 -0
- tests/unit/mcp/test_adapter_registry.py +158 -0
- tests/unit/mcp/test_config_model.py +146 -0
- hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt +0 -2
- tests/integration/test_mcp_kiro_integration.py +0 -153
- tests/regression/test_mcp_codex_backup_integration.py +0 -162
- tests/regression/test_mcp_codex_host_strategy.py +0 -163
- tests/regression/test_mcp_codex_model_validation.py +0 -117
- tests/regression/test_mcp_kiro_backup_integration.py +0 -241
- tests/regression/test_mcp_kiro_cli_integration.py +0 -141
- tests/regression/test_mcp_kiro_decorator_registration.py +0 -71
- tests/regression/test_mcp_kiro_host_strategy.py +0 -214
- tests/regression/test_mcp_kiro_model_validation.py +0 -116
- tests/regression/test_mcp_kiro_omni_conversion.py +0 -104
- tests/test_mcp_atomic_operations.py +0 -276
- tests/test_mcp_backup_integration.py +0 -308
- tests/test_mcp_cli_all_host_specific_args.py +0 -496
- tests/test_mcp_cli_backup_management.py +0 -295
- tests/test_mcp_cli_direct_management.py +0 -456
- tests/test_mcp_cli_discovery_listing.py +0 -582
- tests/test_mcp_cli_host_config_integration.py +0 -823
- tests/test_mcp_cli_package_management.py +0 -360
- tests/test_mcp_cli_partial_updates.py +0 -859
- tests/test_mcp_environment_integration.py +0 -520
- tests/test_mcp_host_config_backup.py +0 -257
- tests/test_mcp_host_configuration_manager.py +0 -331
- tests/test_mcp_host_registry_decorator.py +0 -348
- tests/test_mcp_pydantic_architecture_v4.py +0 -603
- tests/test_mcp_server_config_models.py +0 -242
- tests/test_mcp_server_config_type_field.py +0 -221
- tests/test_mcp_sync_functionality.py +0 -316
- tests/test_mcp_user_feedback_reporting.py +0 -359
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
|
hatch/mcp_host_config/backup.py
CHANGED
|
@@ -355,10 +355,12 @@ class MCPHostConfigBackupManager:
|
|
|
355
355
|
|
|
356
356
|
backups = []
|
|
357
357
|
|
|
358
|
-
# Search for
|
|
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"
|
|
361
|
-
f"mcp.json.MCPHostType.{hostname.upper()}.*" # Legacy incorrect format
|
|
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
|
+
|