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
@@ -3,17 +3,17 @@
3
3
  This module provides MCP host configuration management functionality,
4
4
  including backup and restore capabilities for MCP server configurations,
5
5
  decorator-based strategy registration, and consolidated Pydantic models.
6
+
7
+ Architecture Notes (v2.0 - Unified Adapter Architecture):
8
+ - MCPServerConfig is the single unified model for all MCP configurations
9
+ - Host-specific serialization is handled by adapters in hatch/mcp_host_config/adapters/
10
+ - Legacy host-specific models (MCPServerConfigGemini, etc.) have been removed
6
11
  """
7
12
 
8
13
  from .backup import MCPHostConfigBackupManager
9
14
  from .models import (
10
15
  MCPHostType, MCPServerConfig, HostConfiguration, EnvironmentData,
11
16
  PackageHostConfiguration, EnvironmentPackageEntry, ConfigurationResult, SyncResult,
12
- # Host-specific configuration models
13
- MCPServerConfigBase, MCPServerConfigGemini, MCPServerConfigVSCode,
14
- MCPServerConfigCursor, MCPServerConfigClaude, MCPServerConfigKiro,
15
- MCPServerConfigCodex, MCPServerConfigOmni,
16
- HOST_MODEL_REGISTRY
17
17
  )
18
18
  from .host_management import (
19
19
  MCPHostRegistry, MCPHostStrategy, MCPHostConfigurationManager, register_host_strategy
@@ -21,20 +21,20 @@ from .host_management import (
21
21
  from .reporting import (
22
22
  FieldOperation, ConversionReport, generate_conversion_report, display_report
23
23
  )
24
+ from .adapters import AdapterRegistry, get_adapter, get_default_registry
24
25
 
25
26
  # Import strategies to trigger decorator registration
26
27
  from . import strategies
27
28
 
28
29
  __all__ = [
29
30
  'MCPHostConfigBackupManager',
31
+ # Core models
30
32
  'MCPHostType', 'MCPServerConfig', 'HostConfiguration', 'EnvironmentData',
31
33
  'PackageHostConfiguration', 'EnvironmentPackageEntry', 'ConfigurationResult', 'SyncResult',
32
- # Host-specific configuration models
33
- 'MCPServerConfigBase', 'MCPServerConfigGemini', 'MCPServerConfigVSCode',
34
- 'MCPServerConfigCursor', 'MCPServerConfigClaude', 'MCPServerConfigKiro',
35
- 'MCPServerConfigCodex', 'MCPServerConfigOmni',
36
- 'HOST_MODEL_REGISTRY',
34
+ # Adapter architecture
35
+ 'AdapterRegistry', 'get_adapter', 'get_default_registry',
37
36
  # User feedback reporting
38
37
  'FieldOperation', 'ConversionReport', 'generate_conversion_report', 'display_report',
38
+ # Host management
39
39
  'MCPHostRegistry', 'MCPHostStrategy', 'MCPHostConfigurationManager', 'register_host_strategy'
40
40
  ]
@@ -0,0 +1,34 @@
1
+ """MCP Host Config Adapters.
2
+
3
+ This module provides host-specific adapters for the Unified Adapter Architecture.
4
+ Each adapter handles validation and serialization for a specific MCP host.
5
+ """
6
+
7
+ from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter
8
+ from hatch.mcp_host_config.adapters.claude import ClaudeAdapter
9
+ from hatch.mcp_host_config.adapters.codex import CodexAdapter
10
+ from hatch.mcp_host_config.adapters.cursor import CursorAdapter
11
+ from hatch.mcp_host_config.adapters.gemini import GeminiAdapter
12
+ from hatch.mcp_host_config.adapters.kiro import KiroAdapter
13
+ from hatch.mcp_host_config.adapters.lmstudio import LMStudioAdapter
14
+ from hatch.mcp_host_config.adapters.registry import AdapterRegistry, get_adapter, get_default_registry
15
+ from hatch.mcp_host_config.adapters.vscode import VSCodeAdapter
16
+
17
+ __all__ = [
18
+ # Base classes and exceptions
19
+ "AdapterValidationError",
20
+ "BaseAdapter",
21
+ # Registry
22
+ "AdapterRegistry",
23
+ "get_adapter",
24
+ "get_default_registry",
25
+ # Host-specific adapters
26
+ "ClaudeAdapter",
27
+ "CodexAdapter",
28
+ "CursorAdapter",
29
+ "GeminiAdapter",
30
+ "KiroAdapter",
31
+ "LMStudioAdapter",
32
+ "VSCodeAdapter",
33
+ ]
34
+
@@ -0,0 +1,170 @@
1
+ """Base adapter class for MCP host configurations.
2
+
3
+ This module defines the abstract BaseAdapter class that all host-specific
4
+ adapters must implement. The adapter pattern allows for:
5
+ - Host-specific validation rules
6
+ - Host-specific serialization format
7
+ - Unified interface across all hosts
8
+ """
9
+
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any, Dict, FrozenSet, List, Optional
12
+
13
+ from hatch.mcp_host_config.models import MCPServerConfig
14
+ from hatch.mcp_host_config.fields import EXCLUDED_ALWAYS
15
+
16
+
17
+ class AdapterValidationError(Exception):
18
+ """Raised when adapter validation fails.
19
+
20
+ Attributes:
21
+ message: Human-readable error message
22
+ field: The field that caused the error (if applicable)
23
+ host_name: The host adapter that raised the error
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ message: str,
29
+ field: Optional[str] = None,
30
+ host_name: Optional[str] = None
31
+ ):
32
+ self.message = message
33
+ self.field = field
34
+ self.host_name = host_name
35
+ super().__init__(self._format_message())
36
+
37
+ def _format_message(self) -> str:
38
+ """Format the error message with optional context."""
39
+ parts = []
40
+ if self.host_name:
41
+ parts.append(f"[{self.host_name}]")
42
+ if self.field:
43
+ parts.append(f"Field '{self.field}':")
44
+ parts.append(self.message)
45
+ return " ".join(parts)
46
+
47
+
48
+ class BaseAdapter(ABC):
49
+ """Abstract base class for host-specific MCP configuration adapters.
50
+
51
+ Each host (Claude Desktop, VSCode, Gemini, etc.) has different requirements
52
+ for MCP server configuration. Adapters handle:
53
+
54
+ 1. **Validation**: Host-specific rules (e.g., "command and url are mutually
55
+ exclusive" for Claude, but not for Gemini which supports triple transport)
56
+
57
+ 2. **Serialization**: Converting MCPServerConfig to the host's expected format
58
+ (field names, structure, excluded fields)
59
+
60
+ 3. **Field Support**: Declaring which fields the host supports
61
+
62
+ Subclasses must implement:
63
+ - host_name: The identifier for this host
64
+ - get_supported_fields(): Fields this host accepts
65
+ - validate(): Host-specific validation logic
66
+ - serialize(): Convert config to host format
67
+
68
+ Example:
69
+ >>> class ClaudeAdapter(BaseAdapter):
70
+ ... @property
71
+ ... def host_name(self) -> str:
72
+ ... return "claude-desktop"
73
+ ...
74
+ ... def get_supported_fields(self) -> FrozenSet[str]:
75
+ ... return frozenset({"command", "args", "env", "url", "headers", "type"})
76
+ ...
77
+ ... def validate(self, config: MCPServerConfig) -> None:
78
+ ... if config.command and config.url:
79
+ ... raise AdapterValidationError("Cannot have both command and url")
80
+ ...
81
+ ... def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
82
+ ... return {k: v for k, v in config.model_dump().items() if v is not None}
83
+ """
84
+
85
+ @property
86
+ @abstractmethod
87
+ def host_name(self) -> str:
88
+ """Return the identifier for this host.
89
+
90
+ Returns:
91
+ Host identifier string (e.g., "claude-desktop", "vscode", "gemini")
92
+ """
93
+ ...
94
+
95
+ @abstractmethod
96
+ def get_supported_fields(self) -> FrozenSet[str]:
97
+ """Return the set of fields supported by this host.
98
+
99
+ Returns:
100
+ FrozenSet of field names that this host accepts.
101
+ Fields not in this set will be filtered during serialization.
102
+ """
103
+ ...
104
+
105
+ @abstractmethod
106
+ def validate(self, config: MCPServerConfig) -> None:
107
+ """Validate the configuration for this host.
108
+
109
+ This method should check host-specific rules and raise
110
+ AdapterValidationError if the configuration is invalid.
111
+
112
+ Args:
113
+ config: The MCPServerConfig to validate
114
+
115
+ Raises:
116
+ AdapterValidationError: If validation fails
117
+ """
118
+ ...
119
+
120
+ @abstractmethod
121
+ def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
122
+ """Serialize the configuration for this host.
123
+
124
+ This method should convert the MCPServerConfig to the format
125
+ expected by the host's configuration file.
126
+
127
+ Args:
128
+ config: The MCPServerConfig to serialize
129
+
130
+ Returns:
131
+ Dictionary in the host's expected format
132
+ """
133
+ ...
134
+
135
+ def get_excluded_fields(self) -> FrozenSet[str]:
136
+ """Return fields that should always be excluded from serialization.
137
+
138
+ By default, returns EXCLUDED_ALWAYS (e.g., 'name' which is Hatch metadata).
139
+ Subclasses can override to add host-specific exclusions.
140
+
141
+ Returns:
142
+ FrozenSet of field names to exclude
143
+ """
144
+ return EXCLUDED_ALWAYS
145
+
146
+ def filter_fields(self, config: MCPServerConfig) -> Dict[str, Any]:
147
+ """Filter config to only include supported, non-excluded, non-None fields.
148
+
149
+ This is a helper method for serialization that:
150
+ 1. Gets all fields from the config
151
+ 2. Filters to only supported fields
152
+ 3. Removes excluded fields
153
+ 4. Removes None values
154
+
155
+ Args:
156
+ config: The MCPServerConfig to filter
157
+
158
+ Returns:
159
+ Dictionary with only valid fields for this host
160
+ """
161
+ supported = self.get_supported_fields()
162
+ excluded = self.get_excluded_fields()
163
+
164
+ result = {}
165
+ for field, value in config.model_dump(exclude_none=True).items():
166
+ if field in supported and field not in excluded:
167
+ result[field] = value
168
+
169
+ return result
170
+
@@ -0,0 +1,105 @@
1
+ """Claude Desktop/Code adapter for MCP host configuration.
2
+
3
+ Claude Desktop and Claude Code share the same configuration format:
4
+ - Supports 'type' field for transport discrimination
5
+ - Mutually exclusive: command XOR url (never both)
6
+ - Standard field set: command, args, env, url, headers, type
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 CLAUDE_FIELDS
13
+ from hatch.mcp_host_config.models import MCPServerConfig
14
+
15
+
16
+ class ClaudeAdapter(BaseAdapter):
17
+ """Adapter for Claude Desktop and Claude Code hosts.
18
+
19
+ Claude uses a strict validation model:
20
+ - Local servers: command (required), args, env
21
+ - Remote servers: url (required), headers, env
22
+ - Never both command and url
23
+
24
+ Supports the 'type' field for explicit transport discrimination.
25
+ """
26
+
27
+ def __init__(self, variant: str = "desktop"):
28
+ """Initialize Claude adapter.
29
+
30
+ Args:
31
+ variant: Either "desktop" or "code" to specify the Claude variant.
32
+ """
33
+ if variant not in ("desktop", "code"):
34
+ raise ValueError(f"Invalid Claude variant: {variant}. Must be 'desktop' or 'code'")
35
+ self._variant = variant
36
+
37
+ @property
38
+ def host_name(self) -> str:
39
+ """Return the host identifier."""
40
+ return f"claude-{self._variant}"
41
+
42
+ def get_supported_fields(self) -> FrozenSet[str]:
43
+ """Return fields supported by Claude."""
44
+ return CLAUDE_FIELDS
45
+
46
+ def validate(self, config: MCPServerConfig) -> None:
47
+ """Validate configuration for Claude.
48
+
49
+ Claude requires exactly one transport:
50
+ - stdio (command)
51
+ - sse (url)
52
+
53
+ Having both command and url is invalid.
54
+ """
55
+ has_command = config.command is not None
56
+ has_url = config.url is not None
57
+ has_http_url = config.httpUrl is not None
58
+
59
+ # Claude doesn't support httpUrl
60
+ if has_http_url:
61
+ raise AdapterValidationError(
62
+ "httpUrl is not supported (use 'url' for remote servers)",
63
+ field="httpUrl",
64
+ host_name=self.host_name
65
+ )
66
+
67
+ # Must have exactly one transport
68
+ if not has_command and not has_url:
69
+ raise AdapterValidationError(
70
+ "Either 'command' (local) or 'url' (remote) must be specified",
71
+ host_name=self.host_name
72
+ )
73
+
74
+ if has_command and has_url:
75
+ raise AdapterValidationError(
76
+ "Cannot specify both 'command' and 'url' - choose one transport",
77
+ host_name=self.host_name
78
+ )
79
+
80
+ # Validate type consistency if specified
81
+ if config.type is not None:
82
+ if config.type == "stdio" and not has_command:
83
+ raise AdapterValidationError(
84
+ "type='stdio' requires 'command' field",
85
+ field="type",
86
+ host_name=self.host_name
87
+ )
88
+ if config.type in ("sse", "http") and not has_url:
89
+ raise AdapterValidationError(
90
+ f"type='{config.type}' requires 'url' field",
91
+ field="type",
92
+ host_name=self.host_name
93
+ )
94
+
95
+ def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
96
+ """Serialize configuration for Claude format.
97
+
98
+ Returns a dictionary suitable for Claude's config.json format.
99
+ """
100
+ # Validate before serializing
101
+ self.validate(config)
102
+
103
+ # Filter to supported fields
104
+ return self.filter_fields(config)
105
+
@@ -0,0 +1,104 @@
1
+ """Codex CLI adapter for MCP host configuration.
2
+
3
+ Codex CLI has unique features:
4
+ - No 'type' field support
5
+ - Field name mappings: args→arguments, headers→http_headers
6
+ - Rich configuration: timeouts, env_vars, tool management, bearer tokens
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 CODEX_FIELDS, CODEX_FIELD_MAPPINGS
13
+ from hatch.mcp_host_config.models import MCPServerConfig
14
+
15
+
16
+ class CodexAdapter(BaseAdapter):
17
+ """Adapter for Codex CLI MCP host.
18
+
19
+ Codex uses different field names than other hosts:
20
+ - 'args' → 'arguments'
21
+ - 'headers' → 'http_headers'
22
+
23
+ Codex also has:
24
+ - Working directory support (cwd)
25
+ - Timeout configuration (startup_timeout_sec, tool_timeout_sec)
26
+ - Server enable/disable (enabled)
27
+ - Tool filtering (enabled_tools, disabled_tools)
28
+ - Bearer token support (bearer_token_env_var)
29
+ """
30
+
31
+ @property
32
+ def host_name(self) -> str:
33
+ """Return the host identifier."""
34
+ return "codex"
35
+
36
+ def get_supported_fields(self) -> FrozenSet[str]:
37
+ """Return fields supported by Codex."""
38
+ return CODEX_FIELDS
39
+
40
+ def validate(self, config: MCPServerConfig) -> None:
41
+ """Validate configuration for Codex.
42
+
43
+ Codex requires exactly one transport (command XOR url).
44
+ Does not support 'type' field.
45
+ """
46
+ has_command = config.command is not None
47
+ has_url = config.url is not None
48
+ has_http_url = config.httpUrl is not None
49
+
50
+ # Codex doesn't support httpUrl
51
+ if has_http_url:
52
+ raise AdapterValidationError(
53
+ "httpUrl is not supported (use 'url' for remote servers)",
54
+ field="httpUrl",
55
+ host_name=self.host_name
56
+ )
57
+
58
+ # Must have exactly one transport
59
+ if not has_command and not has_url:
60
+ raise AdapterValidationError(
61
+ "Either 'command' (local) or 'url' (remote) must be specified",
62
+ host_name=self.host_name
63
+ )
64
+
65
+ if has_command and has_url:
66
+ raise AdapterValidationError(
67
+ "Cannot specify both 'command' and 'url' - choose one transport",
68
+ host_name=self.host_name
69
+ )
70
+
71
+ # 'type' field is not supported by Codex
72
+ if config.type is not None:
73
+ raise AdapterValidationError(
74
+ "'type' field is not supported by Codex CLI",
75
+ field="type",
76
+ host_name=self.host_name
77
+ )
78
+
79
+ # Validate enabled_tools and disabled_tools mutual exclusion
80
+ if config.enabled_tools is not None and config.disabled_tools is not None:
81
+ raise AdapterValidationError(
82
+ "Cannot specify both 'enabled_tools' and 'disabled_tools'",
83
+ host_name=self.host_name
84
+ )
85
+
86
+ def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
87
+ """Serialize configuration for Codex format.
88
+
89
+ Applies field mappings:
90
+ - args → arguments
91
+ - headers → http_headers
92
+ """
93
+ self.validate(config)
94
+
95
+ # Get base filtered fields
96
+ result = self.filter_fields(config)
97
+
98
+ # Apply field mappings
99
+ for universal_name, codex_name in CODEX_FIELD_MAPPINGS.items():
100
+ if universal_name in result:
101
+ result[codex_name] = result.pop(universal_name)
102
+
103
+ return result
104
+
@@ -0,0 +1,83 @@
1
+ """Cursor adapter for MCP host configuration.
2
+
3
+ Cursor is similar to VSCode but with limited additional fields:
4
+ - envFile: Path to environment file (like VSCode)
5
+ - No 'inputs' field support (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 CURSOR_FIELDS
12
+ from hatch.mcp_host_config.models import MCPServerConfig
13
+
14
+
15
+ class CursorAdapter(BaseAdapter):
16
+ """Adapter for Cursor MCP host.
17
+
18
+ Cursor is like a simplified VSCode:
19
+ - Supports Claude base fields + envFile
20
+ - Does NOT support inputs (VSCode-only feature)
21
+ - Requires exactly one transport (command XOR url)
22
+ """
23
+
24
+ @property
25
+ def host_name(self) -> str:
26
+ """Return the host identifier."""
27
+ return "cursor"
28
+
29
+ def get_supported_fields(self) -> FrozenSet[str]:
30
+ """Return fields supported by Cursor."""
31
+ return CURSOR_FIELDS
32
+
33
+ def validate(self, config: MCPServerConfig) -> None:
34
+ """Validate configuration for Cursor.
35
+
36
+ Same rules as Claude: exactly one transport required.
37
+ Warns if 'inputs' is specified (not supported).
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
+ # Cursor 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 Cursor format."""
81
+ self.validate(config)
82
+ return self.filter_fields(config)
83
+
@@ -0,0 +1,75 @@
1
+ """Gemini CLI adapter for MCP host configuration.
2
+
3
+ Gemini has unique features:
4
+ - Triple transport: command (stdio), url (SSE), httpUrl (HTTP streaming)
5
+ - Multiple transports can coexist (not mutually exclusive)
6
+ - No 'type' field support
7
+ - Rich OAuth configuration
8
+ - Working directory, timeout, trust settings
9
+ """
10
+
11
+ from typing import Any, Dict, FrozenSet
12
+
13
+ from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter
14
+ from hatch.mcp_host_config.fields import GEMINI_FIELDS
15
+ from hatch.mcp_host_config.models import MCPServerConfig
16
+
17
+
18
+ class GeminiAdapter(BaseAdapter):
19
+ """Adapter for Gemini CLI MCP host.
20
+
21
+ Gemini is unique among MCP hosts:
22
+ - Supports THREE transport types (stdio, SSE, HTTP streaming)
23
+ - Transports are NOT mutually exclusive (can have multiple)
24
+ - Does NOT support 'type' field
25
+ - Has rich configuration: OAuth, timeout, trust, tool filtering
26
+ """
27
+
28
+ @property
29
+ def host_name(self) -> str:
30
+ """Return the host identifier."""
31
+ return "gemini"
32
+
33
+ def get_supported_fields(self) -> FrozenSet[str]:
34
+ """Return fields supported by Gemini."""
35
+ return GEMINI_FIELDS
36
+
37
+ def validate(self, config: MCPServerConfig) -> None:
38
+ """Validate configuration for Gemini.
39
+
40
+ Gemini is flexible:
41
+ - At least one transport is required (command, url, or httpUrl)
42
+ - Multiple transports are allowed
43
+ - 'type' field is not supported
44
+ """
45
+ has_command = config.command is not None
46
+ has_url = config.url is not None
47
+ has_http_url = config.httpUrl is not None
48
+
49
+ # Must have at least one transport
50
+ if not has_command and not has_url and not has_http_url:
51
+ raise AdapterValidationError(
52
+ "At least one transport must be specified: 'command', 'url', or 'httpUrl'",
53
+ host_name=self.host_name
54
+ )
55
+
56
+ # 'type' field is not supported by Gemini
57
+ if config.type is not None:
58
+ raise AdapterValidationError(
59
+ "'type' field is not supported by Gemini CLI",
60
+ field="type",
61
+ host_name=self.host_name
62
+ )
63
+
64
+ # Validate includeTools and excludeTools are mutually exclusive
65
+ if config.includeTools is not None and config.excludeTools is not None:
66
+ raise AdapterValidationError(
67
+ "Cannot specify both 'includeTools' and 'excludeTools'",
68
+ host_name=self.host_name
69
+ )
70
+
71
+ def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
72
+ """Serialize configuration for Gemini format."""
73
+ self.validate(config)
74
+ return self.filter_fields(config)
75
+