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
hatch/mcp_host_config/models.py
CHANGED
|
@@ -29,42 +29,102 @@ class MCPHostType(str, Enum):
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class MCPServerConfig(BaseModel):
|
|
32
|
-
"""
|
|
32
|
+
"""Unified MCP server configuration containing ALL possible fields.
|
|
33
|
+
|
|
34
|
+
This is the single source of truth for MCP server configuration. It contains
|
|
35
|
+
fields for ALL hosts. Adapters handle validation and serialization based on
|
|
36
|
+
each host's supported field set.
|
|
37
|
+
|
|
38
|
+
Design Notes:
|
|
39
|
+
- extra="allow" for forward compatibility with unknown host fields
|
|
40
|
+
- Minimal validation (adapters do host-specific validation)
|
|
41
|
+
- 'name' field is Hatch metadata, never serialized to host configs
|
|
42
|
+
"""
|
|
33
43
|
|
|
34
44
|
model_config = ConfigDict(extra="allow")
|
|
35
45
|
|
|
36
|
-
#
|
|
46
|
+
# ========================================================================
|
|
47
|
+
# Hatch Metadata (never serialized to host config files)
|
|
48
|
+
# ========================================================================
|
|
37
49
|
name: Optional[str] = Field(None, description="Server name for identification")
|
|
38
50
|
|
|
39
|
-
#
|
|
51
|
+
# ========================================================================
|
|
52
|
+
# Transport Fields (mutually exclusive at validation, but all present)
|
|
53
|
+
# ========================================================================
|
|
54
|
+
|
|
55
|
+
# Transport type discriminator (Claude/VSCode/Cursor only, NOT Gemini/Kiro/Codex)
|
|
40
56
|
type: Optional[Literal["stdio", "sse", "http"]] = Field(
|
|
41
57
|
None,
|
|
42
58
|
description="Transport type (stdio for local, sse/http for remote)"
|
|
43
59
|
)
|
|
44
60
|
|
|
45
|
-
#
|
|
61
|
+
# stdio transport (local server)
|
|
46
62
|
command: Optional[str] = Field(None, description="Executable path/name for local servers")
|
|
47
63
|
args: Optional[List[str]] = Field(None, description="Command arguments for local servers")
|
|
48
|
-
env: Optional[Dict[str, str]] = Field(None, description="Environment variables for all transports")
|
|
49
64
|
|
|
50
|
-
#
|
|
51
|
-
url: Optional[str] = Field(None, description="Server endpoint URL
|
|
65
|
+
# sse transport (remote server)
|
|
66
|
+
url: Optional[str] = Field(None, description="Server endpoint URL (SSE transport)")
|
|
67
|
+
|
|
68
|
+
# http transport (Gemini-specific remote server)
|
|
69
|
+
httpUrl: Optional[str] = Field(None, description="HTTP streaming endpoint URL (Gemini)")
|
|
70
|
+
|
|
71
|
+
# ========================================================================
|
|
72
|
+
# Universal Fields (all hosts)
|
|
73
|
+
# ========================================================================
|
|
74
|
+
env: Optional[Dict[str, str]] = Field(None, description="Environment variables")
|
|
52
75
|
headers: Optional[Dict[str, str]] = Field(None, description="HTTP headers for remote servers")
|
|
53
|
-
|
|
54
|
-
@model_validator(mode='after')
|
|
55
|
-
def validate_server_type(self):
|
|
56
|
-
"""Validate that either local or remote configuration is provided, not both."""
|
|
57
|
-
command = self.command
|
|
58
|
-
url = self.url
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
# ========================================================================
|
|
78
|
+
# Gemini-Specific Fields
|
|
79
|
+
# ========================================================================
|
|
80
|
+
cwd: Optional[str] = Field(None, description="Working directory (Gemini/Codex)")
|
|
81
|
+
timeout: Optional[int] = Field(None, description="Request timeout in milliseconds")
|
|
82
|
+
trust: Optional[bool] = Field(None, description="Bypass tool call confirmations")
|
|
83
|
+
includeTools: Optional[List[str]] = Field(None, description="Tools to include (allowlist)")
|
|
84
|
+
excludeTools: Optional[List[str]] = Field(None, description="Tools to exclude (blocklist)")
|
|
62
85
|
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
# OAuth configuration (Gemini)
|
|
87
|
+
oauth_enabled: Optional[bool] = Field(None, description="Enable OAuth for this server")
|
|
88
|
+
oauth_clientId: Optional[str] = Field(None, description="OAuth client identifier")
|
|
89
|
+
oauth_clientSecret: Optional[str] = Field(None, description="OAuth client secret")
|
|
90
|
+
oauth_authorizationUrl: Optional[str] = Field(None, description="OAuth authorization endpoint")
|
|
91
|
+
oauth_tokenUrl: Optional[str] = Field(None, description="OAuth token endpoint")
|
|
92
|
+
oauth_scopes: Optional[List[str]] = Field(None, description="Required OAuth scopes")
|
|
93
|
+
oauth_redirectUri: Optional[str] = Field(None, description="Custom redirect URI")
|
|
94
|
+
oauth_tokenParamName: Optional[str] = Field(None, description="Query parameter name for tokens")
|
|
95
|
+
oauth_audiences: Optional[List[str]] = Field(None, description="OAuth audiences")
|
|
96
|
+
authProviderType: Optional[str] = Field(None, description="Authentication provider type")
|
|
97
|
+
|
|
98
|
+
# ========================================================================
|
|
99
|
+
# VSCode/Cursor-Specific Fields
|
|
100
|
+
# ========================================================================
|
|
101
|
+
envFile: Optional[str] = Field(None, description="Path to environment file")
|
|
102
|
+
inputs: Optional[List[Dict]] = Field(None, description="Input variable definitions (VSCode only)")
|
|
103
|
+
|
|
104
|
+
# ========================================================================
|
|
105
|
+
# Kiro-Specific Fields
|
|
106
|
+
# ========================================================================
|
|
107
|
+
disabled: Optional[bool] = Field(None, description="Whether server is disabled")
|
|
108
|
+
autoApprove: Optional[List[str]] = Field(None, description="Auto-approved tool names")
|
|
109
|
+
disabledTools: Optional[List[str]] = Field(None, description="Disabled tool names")
|
|
110
|
+
|
|
111
|
+
# ========================================================================
|
|
112
|
+
# Codex-Specific Fields
|
|
113
|
+
# ========================================================================
|
|
114
|
+
env_vars: Optional[List[str]] = Field(None, description="Environment variables to whitelist/forward")
|
|
115
|
+
startup_timeout_sec: Optional[int] = Field(None, description="Server startup timeout in seconds")
|
|
116
|
+
tool_timeout_sec: Optional[int] = Field(None, description="Tool execution timeout in seconds")
|
|
117
|
+
enabled: Optional[bool] = Field(None, description="Enable/disable server without deleting config")
|
|
118
|
+
enabled_tools: Optional[List[str]] = Field(None, description="Allow-list of tools to expose")
|
|
119
|
+
disabled_tools: Optional[List[str]] = Field(None, description="Deny-list of tools to hide")
|
|
120
|
+
bearer_token_env_var: Optional[str] = Field(None, description="Env var containing bearer token")
|
|
121
|
+
http_headers: Optional[Dict[str, str]] = Field(None, description="HTTP headers (Codex naming)")
|
|
122
|
+
env_http_headers: Optional[Dict[str, str]] = Field(None, description="Header names to env var names")
|
|
123
|
+
|
|
124
|
+
# ========================================================================
|
|
125
|
+
# Minimal Validators (host-specific validation is in adapters)
|
|
126
|
+
# ========================================================================
|
|
65
127
|
|
|
66
|
-
return self
|
|
67
|
-
|
|
68
128
|
@field_validator('command')
|
|
69
129
|
@classmethod
|
|
70
130
|
def validate_command_not_empty(cls, v):
|
|
@@ -73,7 +133,7 @@ class MCPServerConfig(BaseModel):
|
|
|
73
133
|
raise ValueError("Command cannot be empty")
|
|
74
134
|
return v.strip() if v else v
|
|
75
135
|
|
|
76
|
-
@field_validator('url')
|
|
136
|
+
@field_validator('url', 'httpUrl')
|
|
77
137
|
@classmethod
|
|
78
138
|
def validate_url_format(cls, v):
|
|
79
139
|
"""Validate URL format when provided."""
|
|
@@ -83,53 +143,98 @@ class MCPServerConfig(BaseModel):
|
|
|
83
143
|
return v
|
|
84
144
|
|
|
85
145
|
@model_validator(mode='after')
|
|
86
|
-
def
|
|
87
|
-
"""Validate
|
|
88
|
-
# Validate args are only provided with command
|
|
89
|
-
if self.args is not None and self.command is None:
|
|
90
|
-
raise ValueError("'args' can only be specified with 'command' for local servers")
|
|
91
|
-
|
|
92
|
-
# Validate headers are only provided with URL
|
|
93
|
-
if self.headers is not None and self.url is None:
|
|
94
|
-
raise ValueError("'headers' can only be specified with 'url' for remote servers")
|
|
146
|
+
def validate_has_transport(self):
|
|
147
|
+
"""Validate that at least one transport is configured.
|
|
95
148
|
|
|
149
|
+
Note: Mutual exclusion validation is done by adapters, not here.
|
|
150
|
+
This allows the unified model to be flexible while adapters enforce
|
|
151
|
+
host-specific rules.
|
|
152
|
+
"""
|
|
153
|
+
if self.command is None and self.url is None and self.httpUrl is None:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
"At least one transport must be specified: "
|
|
156
|
+
"'command' (stdio), 'url' (sse), or 'httpUrl' (http)"
|
|
157
|
+
)
|
|
96
158
|
return self
|
|
97
159
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Only validate if type field is explicitly set
|
|
102
|
-
if self.type is not None:
|
|
103
|
-
if self.type == "stdio":
|
|
104
|
-
if not self.command:
|
|
105
|
-
raise ValueError("'type=stdio' requires 'command' field")
|
|
106
|
-
if self.url:
|
|
107
|
-
raise ValueError("'type=stdio' cannot be used with 'url' field")
|
|
108
|
-
elif self.type in ("sse", "http"):
|
|
109
|
-
if not self.url:
|
|
110
|
-
raise ValueError(f"'type={self.type}' requires 'url' field")
|
|
111
|
-
if self.command:
|
|
112
|
-
raise ValueError(f"'type={self.type}' cannot be used with 'command' field")
|
|
113
|
-
|
|
114
|
-
return self
|
|
160
|
+
# ========================================================================
|
|
161
|
+
# Transport Detection Properties
|
|
162
|
+
# ========================================================================
|
|
115
163
|
|
|
116
164
|
@property
|
|
117
165
|
def is_local_server(self) -> bool:
|
|
118
|
-
"""Check if this is a local server configuration."""
|
|
119
|
-
|
|
166
|
+
"""Check if this is a local server configuration (stdio transport)."""
|
|
167
|
+
return self.is_stdio()
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def is_remote_server(self) -> bool:
|
|
171
|
+
"""Check if this is a remote server configuration (sse/http transport)."""
|
|
172
|
+
return self.is_sse() or self.is_http()
|
|
173
|
+
|
|
174
|
+
def is_stdio(self) -> bool:
|
|
175
|
+
"""Check if this server uses stdio transport (command-based local server).
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if the server is configured for stdio transport.
|
|
179
|
+
|
|
180
|
+
Priority:
|
|
181
|
+
1. Explicit type="stdio" field takes precedence
|
|
182
|
+
2. Otherwise, presence of 'command' field indicates stdio
|
|
183
|
+
"""
|
|
120
184
|
if self.type is not None:
|
|
121
185
|
return self.type == "stdio"
|
|
122
|
-
# Fall back to command detection for backward compatibility
|
|
123
186
|
return self.command is not None
|
|
124
187
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
188
|
+
def is_sse(self) -> bool:
|
|
189
|
+
"""Check if this server uses SSE transport (URL-based remote server).
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if the server is configured for SSE transport.
|
|
193
|
+
|
|
194
|
+
Priority:
|
|
195
|
+
1. Explicit type="sse" field takes precedence
|
|
196
|
+
2. Otherwise, presence of 'url' field indicates SSE
|
|
197
|
+
"""
|
|
129
198
|
if self.type is not None:
|
|
130
|
-
return self.type
|
|
131
|
-
# Fall back to url detection for backward compatibility
|
|
199
|
+
return self.type == "sse"
|
|
132
200
|
return self.url is not None
|
|
201
|
+
|
|
202
|
+
def is_http(self) -> bool:
|
|
203
|
+
"""Check if this server uses HTTP streaming transport (Gemini-specific).
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True if the server is configured for HTTP streaming transport.
|
|
207
|
+
|
|
208
|
+
Priority:
|
|
209
|
+
1. Explicit type="http" field takes precedence
|
|
210
|
+
2. Otherwise, presence of 'httpUrl' field indicates HTTP streaming
|
|
211
|
+
"""
|
|
212
|
+
if self.type is not None:
|
|
213
|
+
return self.type == "http"
|
|
214
|
+
return self.httpUrl is not None
|
|
215
|
+
|
|
216
|
+
def get_transport_type(self) -> Optional[str]:
|
|
217
|
+
"""Get the transport type for this server configuration.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
"stdio" for command-based local servers
|
|
221
|
+
"sse" for URL-based remote servers (SSE transport)
|
|
222
|
+
"http" for httpUrl-based remote servers (Gemini HTTP streaming)
|
|
223
|
+
None if transport cannot be determined
|
|
224
|
+
"""
|
|
225
|
+
# Explicit type takes precedence
|
|
226
|
+
if self.type is not None:
|
|
227
|
+
return self.type
|
|
228
|
+
|
|
229
|
+
# Infer from fields
|
|
230
|
+
if self.command is not None:
|
|
231
|
+
return "stdio"
|
|
232
|
+
if self.url is not None:
|
|
233
|
+
return "sse"
|
|
234
|
+
if self.httpUrl is not None:
|
|
235
|
+
return "http"
|
|
236
|
+
|
|
237
|
+
return None
|
|
133
238
|
|
|
134
239
|
|
|
135
240
|
|
|
@@ -323,404 +428,4 @@ class SyncResult(BaseModel):
|
|
|
323
428
|
if not self.results:
|
|
324
429
|
return 0.0
|
|
325
430
|
successful = len([r for r in self.results if r.success])
|
|
326
|
-
return (successful / len(self.results)) * 100.0
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
# ============================================================================
|
|
330
|
-
# MCP Host-Specific Configuration Models
|
|
331
|
-
# ============================================================================
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
class MCPServerConfigBase(BaseModel):
|
|
335
|
-
"""Base class for MCP server configurations with universal fields.
|
|
336
|
-
|
|
337
|
-
This model contains fields supported by ALL MCP hosts and provides
|
|
338
|
-
transport validation logic. Host-specific models inherit from this base.
|
|
339
|
-
"""
|
|
340
|
-
|
|
341
|
-
model_config = ConfigDict(extra="forbid")
|
|
342
|
-
|
|
343
|
-
# Hatch-specific field
|
|
344
|
-
name: Optional[str] = Field(None, description="Server name for identification")
|
|
345
|
-
|
|
346
|
-
# Transport type (PRIMARY DISCRIMINATOR)
|
|
347
|
-
type: Optional[Literal["stdio", "sse", "http"]] = Field(
|
|
348
|
-
None,
|
|
349
|
-
description="Transport type (stdio for local, sse/http for remote)"
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
# stdio transport fields
|
|
353
|
-
command: Optional[str] = Field(None, description="Server executable command")
|
|
354
|
-
args: Optional[List[str]] = Field(None, description="Command arguments")
|
|
355
|
-
|
|
356
|
-
# All transports
|
|
357
|
-
env: Optional[Dict[str, str]] = Field(None, description="Environment variables")
|
|
358
|
-
|
|
359
|
-
# Remote transport fields (sse/http)
|
|
360
|
-
url: Optional[str] = Field(None, description="Remote server endpoint")
|
|
361
|
-
headers: Optional[Dict[str, str]] = Field(None, description="HTTP headers")
|
|
362
|
-
|
|
363
|
-
@model_validator(mode='after')
|
|
364
|
-
def validate_transport(self) -> 'MCPServerConfigBase':
|
|
365
|
-
"""Validate transport configuration using type field.
|
|
366
|
-
|
|
367
|
-
Note: Gemini subclass overrides this with dual-transport support.
|
|
368
|
-
"""
|
|
369
|
-
# Skip validation for Gemini which has its own dual-transport validator
|
|
370
|
-
if self.__class__.__name__ == 'MCPServerConfigGemini':
|
|
371
|
-
return self
|
|
372
|
-
|
|
373
|
-
# Check mutual exclusion - command and url cannot both be set
|
|
374
|
-
if self.command is not None and self.url is not None:
|
|
375
|
-
raise ValueError(
|
|
376
|
-
"Cannot specify both 'command' and 'url' - use 'type' field to specify transport"
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
# Validate based on type
|
|
380
|
-
if self.type == "stdio":
|
|
381
|
-
if not self.command:
|
|
382
|
-
raise ValueError("'command' is required for stdio transport")
|
|
383
|
-
elif self.type in ("sse", "http"):
|
|
384
|
-
if not self.url:
|
|
385
|
-
raise ValueError("'url' is required for sse/http transports")
|
|
386
|
-
elif self.type is None:
|
|
387
|
-
# Infer type from fields if not specified
|
|
388
|
-
if self.command:
|
|
389
|
-
self.type = "stdio"
|
|
390
|
-
elif self.url:
|
|
391
|
-
self.type = "sse" # default to sse for remote
|
|
392
|
-
else:
|
|
393
|
-
raise ValueError("Either 'command' or 'url' must be provided")
|
|
394
|
-
|
|
395
|
-
return self
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
class MCPServerConfigGemini(MCPServerConfigBase):
|
|
399
|
-
"""Gemini CLI-specific MCP server configuration.
|
|
400
|
-
|
|
401
|
-
Extends base model with Gemini-specific fields including working directory,
|
|
402
|
-
timeout, trust mode, tool filtering, and OAuth configuration.
|
|
403
|
-
"""
|
|
404
|
-
|
|
405
|
-
# Gemini-specific fields
|
|
406
|
-
cwd: Optional[str] = Field(None, description="Working directory for stdio transport")
|
|
407
|
-
timeout: Optional[int] = Field(None, description="Request timeout in milliseconds")
|
|
408
|
-
trust: Optional[bool] = Field(None, description="Bypass tool call confirmations")
|
|
409
|
-
httpUrl: Optional[str] = Field(None, description="HTTP streaming endpoint URL")
|
|
410
|
-
includeTools: Optional[List[str]] = Field(None, description="Tools to include (allowlist)")
|
|
411
|
-
excludeTools: Optional[List[str]] = Field(None, description="Tools to exclude (blocklist)")
|
|
412
|
-
|
|
413
|
-
# OAuth configuration (simplified - nested object would be better but keeping flat for now)
|
|
414
|
-
oauth_enabled: Optional[bool] = Field(None, description="Enable OAuth for this server")
|
|
415
|
-
oauth_clientId: Optional[str] = Field(None, description="OAuth client identifier")
|
|
416
|
-
oauth_clientSecret: Optional[str] = Field(None, description="OAuth client secret")
|
|
417
|
-
oauth_authorizationUrl: Optional[str] = Field(None, description="OAuth authorization endpoint")
|
|
418
|
-
oauth_tokenUrl: Optional[str] = Field(None, description="OAuth token endpoint")
|
|
419
|
-
oauth_scopes: Optional[List[str]] = Field(None, description="Required OAuth scopes")
|
|
420
|
-
oauth_redirectUri: Optional[str] = Field(None, description="Custom redirect URI")
|
|
421
|
-
oauth_tokenParamName: Optional[str] = Field(None, description="Query parameter name for tokens")
|
|
422
|
-
oauth_audiences: Optional[List[str]] = Field(None, description="OAuth audiences")
|
|
423
|
-
authProviderType: Optional[str] = Field(None, description="Authentication provider type")
|
|
424
|
-
|
|
425
|
-
@model_validator(mode='after')
|
|
426
|
-
def validate_gemini_dual_transport(self):
|
|
427
|
-
"""Override transport validation to support Gemini's dual-transport capability.
|
|
428
|
-
|
|
429
|
-
Gemini supports both:
|
|
430
|
-
- SSE transport with 'url' field
|
|
431
|
-
- HTTP transport with 'httpUrl' field
|
|
432
|
-
|
|
433
|
-
Validates that:
|
|
434
|
-
1. Either url or httpUrl is provided (not both)
|
|
435
|
-
2. Type field matches the transport being used
|
|
436
|
-
"""
|
|
437
|
-
# Check if both url and httpUrl are provided
|
|
438
|
-
if self.url is not None and self.httpUrl is not None:
|
|
439
|
-
raise ValueError("Cannot specify both 'url' and 'httpUrl' - choose one transport")
|
|
440
|
-
|
|
441
|
-
# Validate based on type
|
|
442
|
-
if self.type == "stdio":
|
|
443
|
-
if not self.command:
|
|
444
|
-
raise ValueError("'command' is required for stdio transport")
|
|
445
|
-
elif self.type == "sse":
|
|
446
|
-
if not self.url:
|
|
447
|
-
raise ValueError("'url' is required for sse transport")
|
|
448
|
-
elif self.type == "http":
|
|
449
|
-
if not self.httpUrl:
|
|
450
|
-
raise ValueError("'httpUrl' is required for http transport")
|
|
451
|
-
elif self.type is None:
|
|
452
|
-
# Infer type from fields if not specified
|
|
453
|
-
if self.command:
|
|
454
|
-
self.type = "stdio"
|
|
455
|
-
elif self.url:
|
|
456
|
-
self.type = "sse" # default to sse for url
|
|
457
|
-
elif self.httpUrl:
|
|
458
|
-
self.type = "http" # http for httpUrl
|
|
459
|
-
else:
|
|
460
|
-
raise ValueError("Either 'command', 'url', or 'httpUrl' must be provided")
|
|
461
|
-
|
|
462
|
-
return self
|
|
463
|
-
|
|
464
|
-
@classmethod
|
|
465
|
-
def from_omni(cls, omni: 'MCPServerConfigOmni') -> 'MCPServerConfigGemini':
|
|
466
|
-
"""Convert Omni model to Gemini-specific model using Pydantic APIs."""
|
|
467
|
-
# Get supported fields dynamically from model definition
|
|
468
|
-
supported_fields = set(cls.model_fields.keys())
|
|
469
|
-
|
|
470
|
-
# Use Pydantic's model_dump with include and exclude_unset
|
|
471
|
-
gemini_data = omni.model_dump(include=supported_fields, exclude_unset=True)
|
|
472
|
-
|
|
473
|
-
# Use Pydantic's model_validate for type-safe creation
|
|
474
|
-
return cls.model_validate(gemini_data)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
class MCPServerConfigVSCode(MCPServerConfigBase):
|
|
478
|
-
"""VS Code-specific MCP server configuration.
|
|
479
|
-
|
|
480
|
-
Extends base model with VS Code-specific fields including environment file
|
|
481
|
-
path and input variable definitions.
|
|
482
|
-
"""
|
|
483
|
-
|
|
484
|
-
# VS Code-specific fields
|
|
485
|
-
envFile: Optional[str] = Field(None, description="Path to environment file")
|
|
486
|
-
inputs: Optional[List[Dict]] = Field(None, description="Input variable definitions")
|
|
487
|
-
|
|
488
|
-
@classmethod
|
|
489
|
-
def from_omni(cls, omni: 'MCPServerConfigOmni') -> 'MCPServerConfigVSCode':
|
|
490
|
-
"""Convert Omni model to VS Code-specific model."""
|
|
491
|
-
# Get supported fields dynamically
|
|
492
|
-
supported_fields = set(cls.model_fields.keys())
|
|
493
|
-
|
|
494
|
-
# Single-call field filtering
|
|
495
|
-
vscode_data = omni.model_dump(include=supported_fields, exclude_unset=True)
|
|
496
|
-
|
|
497
|
-
return cls.model_validate(vscode_data)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
class MCPServerConfigCursor(MCPServerConfigBase):
|
|
501
|
-
"""Cursor/LM Studio-specific MCP server configuration.
|
|
502
|
-
|
|
503
|
-
Extends base model with Cursor-specific fields including environment file path.
|
|
504
|
-
Cursor handles config interpolation (${env:NAME}, ${userHome}, etc.) at runtime.
|
|
505
|
-
"""
|
|
506
|
-
|
|
507
|
-
# Cursor-specific fields
|
|
508
|
-
envFile: Optional[str] = Field(None, description="Path to environment file")
|
|
509
|
-
|
|
510
|
-
@classmethod
|
|
511
|
-
def from_omni(cls, omni: 'MCPServerConfigOmni') -> 'MCPServerConfigCursor':
|
|
512
|
-
"""Convert Omni model to Cursor-specific model."""
|
|
513
|
-
# Get supported fields dynamically
|
|
514
|
-
supported_fields = set(cls.model_fields.keys())
|
|
515
|
-
|
|
516
|
-
# Single-call field filtering
|
|
517
|
-
cursor_data = omni.model_dump(include=supported_fields, exclude_unset=True)
|
|
518
|
-
|
|
519
|
-
return cls.model_validate(cursor_data)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
class MCPServerConfigClaude(MCPServerConfigBase):
|
|
523
|
-
"""Claude Desktop/Code-specific MCP server configuration.
|
|
524
|
-
|
|
525
|
-
Uses only universal fields from base model. Supports all transport types
|
|
526
|
-
(stdio, sse, http). Claude handles environment variable expansion at runtime.
|
|
527
|
-
"""
|
|
528
|
-
|
|
529
|
-
# No host-specific fields - uses universal fields only
|
|
530
|
-
|
|
531
|
-
@classmethod
|
|
532
|
-
def from_omni(cls, omni: 'MCPServerConfigOmni') -> 'MCPServerConfigClaude':
|
|
533
|
-
"""Convert Omni model to Claude-specific model."""
|
|
534
|
-
# Get supported fields dynamically
|
|
535
|
-
supported_fields = set(cls.model_fields.keys())
|
|
536
|
-
|
|
537
|
-
# Single-call field filtering
|
|
538
|
-
claude_data = omni.model_dump(include=supported_fields, exclude_unset=True)
|
|
539
|
-
|
|
540
|
-
return cls.model_validate(claude_data)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
class MCPServerConfigKiro(MCPServerConfigBase):
|
|
544
|
-
"""Kiro IDE-specific MCP server configuration.
|
|
545
|
-
|
|
546
|
-
Extends base model with Kiro-specific fields for server management
|
|
547
|
-
and tool control.
|
|
548
|
-
"""
|
|
549
|
-
|
|
550
|
-
# Kiro-specific fields
|
|
551
|
-
disabled: Optional[bool] = Field(None, description="Whether server is disabled")
|
|
552
|
-
autoApprove: Optional[List[str]] = Field(None, description="Auto-approved tool names")
|
|
553
|
-
disabledTools: Optional[List[str]] = Field(None, description="Disabled tool names")
|
|
554
|
-
|
|
555
|
-
@classmethod
|
|
556
|
-
def from_omni(cls, omni: 'MCPServerConfigOmni') -> 'MCPServerConfigKiro':
|
|
557
|
-
"""Convert Omni model to Kiro-specific model."""
|
|
558
|
-
# Get supported fields dynamically
|
|
559
|
-
supported_fields = set(cls.model_fields.keys())
|
|
560
|
-
|
|
561
|
-
# Single-call field filtering
|
|
562
|
-
kiro_data = omni.model_dump(include=supported_fields, exclude_unset=True)
|
|
563
|
-
|
|
564
|
-
return cls.model_validate(kiro_data)
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
628
|
-
Maps universal 'headers' field to Codex-specific 'http_headers' field.
|
|
629
|
-
"""
|
|
630
|
-
supported_fields = set(cls.model_fields.keys())
|
|
631
|
-
codex_data = omni.model_dump(include=supported_fields, exclude_unset=True)
|
|
632
|
-
|
|
633
|
-
# Map shared CLI tool filtering flags (Gemini naming) to Codex naming.
|
|
634
|
-
# This lets `--include-tools/--exclude-tools` work for both Gemini and Codex.
|
|
635
|
-
if getattr(omni, 'includeTools', None) is not None and codex_data.get('enabled_tools') is None:
|
|
636
|
-
codex_data['enabled_tools'] = omni.includeTools
|
|
637
|
-
if getattr(omni, 'excludeTools', None) is not None and codex_data.get('disabled_tools') is None:
|
|
638
|
-
codex_data['disabled_tools'] = omni.excludeTools
|
|
639
|
-
|
|
640
|
-
# Map universal 'headers' to Codex 'http_headers'
|
|
641
|
-
if hasattr(omni, 'headers') and omni.headers is not None:
|
|
642
|
-
codex_data['http_headers'] = omni.headers
|
|
643
|
-
|
|
644
|
-
return cls.model_validate(codex_data)
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
class MCPServerConfigOmni(BaseModel):
|
|
648
|
-
"""Omni configuration supporting all host-specific fields.
|
|
649
|
-
|
|
650
|
-
This is the primary API interface for MCP server configuration. It contains
|
|
651
|
-
all possible fields from all hosts. Use host-specific models' from_omni()
|
|
652
|
-
methods to convert to host-specific configurations.
|
|
653
|
-
"""
|
|
654
|
-
|
|
655
|
-
model_config = ConfigDict(extra="forbid")
|
|
656
|
-
|
|
657
|
-
# Hatch-specific
|
|
658
|
-
name: Optional[str] = None
|
|
659
|
-
|
|
660
|
-
# Universal fields (all hosts)
|
|
661
|
-
type: Optional[Literal["stdio", "sse", "http"]] = None
|
|
662
|
-
command: Optional[str] = None
|
|
663
|
-
args: Optional[List[str]] = None
|
|
664
|
-
env: Optional[Dict[str, str]] = None
|
|
665
|
-
url: Optional[str] = None
|
|
666
|
-
headers: Optional[Dict[str, str]] = None
|
|
667
|
-
|
|
668
|
-
# Gemini CLI specific
|
|
669
|
-
cwd: Optional[str] = None
|
|
670
|
-
timeout: Optional[int] = None
|
|
671
|
-
trust: Optional[bool] = None
|
|
672
|
-
httpUrl: Optional[str] = None
|
|
673
|
-
includeTools: Optional[List[str]] = None
|
|
674
|
-
excludeTools: Optional[List[str]] = None
|
|
675
|
-
oauth_enabled: Optional[bool] = None
|
|
676
|
-
oauth_clientId: Optional[str] = None
|
|
677
|
-
oauth_clientSecret: Optional[str] = None
|
|
678
|
-
oauth_authorizationUrl: Optional[str] = None
|
|
679
|
-
oauth_tokenUrl: Optional[str] = None
|
|
680
|
-
oauth_scopes: Optional[List[str]] = None
|
|
681
|
-
oauth_redirectUri: Optional[str] = None
|
|
682
|
-
oauth_tokenParamName: Optional[str] = None
|
|
683
|
-
oauth_audiences: Optional[List[str]] = None
|
|
684
|
-
authProviderType: Optional[str] = None
|
|
685
|
-
|
|
686
|
-
# VS Code specific
|
|
687
|
-
envFile: Optional[str] = None
|
|
688
|
-
inputs: Optional[List[Dict]] = None
|
|
689
|
-
|
|
690
|
-
# Kiro specific
|
|
691
|
-
disabled: Optional[bool] = None
|
|
692
|
-
autoApprove: Optional[List[str]] = None
|
|
693
|
-
disabledTools: Optional[List[str]] = None
|
|
694
|
-
|
|
695
|
-
# Codex specific
|
|
696
|
-
env_vars: Optional[List[str]] = None
|
|
697
|
-
startup_timeout_sec: Optional[int] = None
|
|
698
|
-
tool_timeout_sec: Optional[int] = None
|
|
699
|
-
enabled: Optional[bool] = None
|
|
700
|
-
enabled_tools: Optional[List[str]] = None
|
|
701
|
-
disabled_tools: Optional[List[str]] = None
|
|
702
|
-
bearer_token_env_var: Optional[str] = None
|
|
703
|
-
env_http_headers: Optional[Dict[str, str]] = None
|
|
704
|
-
# Note: http_headers maps to universal 'headers' field, not a separate Codex field
|
|
705
|
-
|
|
706
|
-
@field_validator('url')
|
|
707
|
-
@classmethod
|
|
708
|
-
def validate_url_format(cls, v):
|
|
709
|
-
"""Validate URL format when provided."""
|
|
710
|
-
if v is not None:
|
|
711
|
-
if not v.startswith(('http://', 'https://')):
|
|
712
|
-
raise ValueError("URL must start with http:// or https://")
|
|
713
|
-
return v
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
# HOST_MODEL_REGISTRY: Dictionary dispatch for host-specific models
|
|
717
|
-
HOST_MODEL_REGISTRY: Dict[MCPHostType, type[MCPServerConfigBase]] = {
|
|
718
|
-
MCPHostType.GEMINI: MCPServerConfigGemini,
|
|
719
|
-
MCPHostType.CLAUDE_DESKTOP: MCPServerConfigClaude,
|
|
720
|
-
MCPHostType.CLAUDE_CODE: MCPServerConfigClaude, # Same as CLAUDE_DESKTOP
|
|
721
|
-
MCPHostType.VSCODE: MCPServerConfigVSCode,
|
|
722
|
-
MCPHostType.CURSOR: MCPServerConfigCursor,
|
|
723
|
-
MCPHostType.LMSTUDIO: MCPServerConfigCursor, # Same as CURSOR
|
|
724
|
-
MCPHostType.KIRO: MCPServerConfigKiro,
|
|
725
|
-
MCPHostType.CODEX: MCPServerConfigCodex,
|
|
726
|
-
}
|
|
431
|
+
return (successful / len(self.results)) * 100.0
|