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
@@ -29,42 +29,102 @@ class MCPHostType(str, Enum):
29
29
 
30
30
 
31
31
  class MCPServerConfig(BaseModel):
32
- """Consolidated MCP server configuration supporting local and remote servers."""
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
- # Server identification
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
- # Transport type (PRIMARY DISCRIMINATOR)
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
- # Local server configuration (Pattern A: Command-Based / stdio transport)
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
- # Remote server configuration (Pattern B: URL-Based / sse/http transports)
51
- url: Optional[str] = Field(None, description="Server endpoint URL for remote servers")
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
- if not command and not url:
61
- raise ValueError("Either 'command' (local server) or 'url' (remote server) must be provided")
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
- if command and url:
64
- raise ValueError("Cannot specify both 'command' and 'url' - choose local or remote server")
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 validate_field_combinations(self):
87
- """Validate field combinations for local vs remote servers."""
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
- @model_validator(mode='after')
99
- def validate_type_field(self):
100
- """Validate type field consistency with command/url fields."""
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
- # Prioritize type field if present
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
- @property
126
- def is_remote_server(self) -> bool:
127
- """Check if this is a remote server configuration."""
128
- # Prioritize type field if present
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 in ("sse", "http")
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