hatch-xclam 0.7.0.dev12__py3-none-any.whl → 0.7.1__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 (87) hide show
  1. hatch/cli_hatch.py +120 -18
  2. hatch/mcp_host_config/__init__.py +4 -2
  3. hatch/mcp_host_config/backup.py +62 -31
  4. hatch/mcp_host_config/models.py +125 -1
  5. hatch/mcp_host_config/strategies.py +268 -1
  6. {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/METADATA +41 -32
  7. hatch_xclam-0.7.1.dist-info/RECORD +105 -0
  8. hatch_xclam-0.7.1.dist-info/top_level.txt +2 -0
  9. tests/integration/__init__.py +5 -0
  10. tests/integration/test_mcp_kiro_integration.py +153 -0
  11. tests/regression/__init__.py +5 -0
  12. tests/regression/test_mcp_codex_backup_integration.py +162 -0
  13. tests/regression/test_mcp_codex_host_strategy.py +163 -0
  14. tests/regression/test_mcp_codex_model_validation.py +117 -0
  15. tests/regression/test_mcp_kiro_backup_integration.py +241 -0
  16. tests/regression/test_mcp_kiro_cli_integration.py +141 -0
  17. tests/regression/test_mcp_kiro_decorator_registration.py +71 -0
  18. tests/regression/test_mcp_kiro_host_strategy.py +214 -0
  19. tests/regression/test_mcp_kiro_model_validation.py +116 -0
  20. tests/regression/test_mcp_kiro_omni_conversion.py +104 -0
  21. tests/test_data_utils.py +108 -0
  22. tests/test_mcp_cli_all_host_specific_args.py +194 -1
  23. tests/test_mcp_cli_direct_management.py +8 -5
  24. hatch_xclam-0.7.0.dev12.dist-info/RECORD +0 -152
  25. hatch_xclam-0.7.0.dev12.dist-info/top_level.txt +0 -3
  26. node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py +0 -45
  27. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSNew.py +0 -365
  28. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSProject.py +0 -206
  29. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSSettings.py +0 -1272
  30. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSSettings_test.py +0 -1547
  31. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSToolFile.py +0 -59
  32. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSUserFile.py +0 -152
  33. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSUtil.py +0 -270
  34. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSVersion.py +0 -574
  35. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/__init__.py +0 -704
  36. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/common.py +0 -709
  37. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/common_test.py +0 -173
  38. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/easy_xml.py +0 -169
  39. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/easy_xml_test.py +0 -113
  40. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/flock_tool.py +0 -55
  41. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/__init__.py +0 -0
  42. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/analyzer.py +0 -805
  43. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/android.py +0 -1172
  44. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/cmake.py +0 -1319
  45. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/compile_commands_json.py +0 -128
  46. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/dump_dependency_json.py +0 -104
  47. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/eclipse.py +0 -462
  48. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/gypd.py +0 -89
  49. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/gypsh.py +0 -56
  50. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py +0 -2745
  51. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/msvs.py +0 -3976
  52. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/msvs_test.py +0 -44
  53. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/ninja.py +0 -2965
  54. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/ninja_test.py +0 -67
  55. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/xcode.py +0 -1391
  56. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/xcode_test.py +0 -26
  57. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py +0 -3112
  58. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input_test.py +0 -99
  59. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/mac_tool.py +0 -767
  60. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/msvs_emulation.py +0 -1260
  61. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/ninja_syntax.py +0 -174
  62. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/simple_copy.py +0 -61
  63. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/win_tool.py +0 -373
  64. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcode_emulation.py +0 -1939
  65. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcode_emulation_test.py +0 -54
  66. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcode_ninja.py +0 -303
  67. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcodeproj_file.py +0 -3196
  68. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xml_fix.py +0 -65
  69. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/__init__.py +0 -15
  70. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_elffile.py +0 -108
  71. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_manylinux.py +0 -252
  72. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_musllinux.py +0 -83
  73. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_parser.py +0 -359
  74. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_structures.py +0 -61
  75. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_tokenizer.py +0 -192
  76. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/markers.py +0 -252
  77. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/metadata.py +0 -825
  78. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/py.typed +0 -0
  79. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/requirements.py +0 -90
  80. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/specifiers.py +0 -1030
  81. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/tags.py +0 -553
  82. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/utils.py +0 -172
  83. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/version.py +0 -563
  84. node_modules/npm/node_modules/node-gyp/gyp/test_gyp.py +0 -261
  85. {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/WHEEL +0 -0
  86. {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/entry_points.txt +0 -0
  87. {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,116 @@
1
+ """
2
+ Kiro MCP Model Validation Tests
3
+
4
+ Tests for MCPServerConfigKiro Pydantic model behavior, field validation,
5
+ and Kiro-specific field combinations.
6
+ """
7
+
8
+ import unittest
9
+ from typing import Optional, List
10
+
11
+ from wobble.decorators import regression_test
12
+
13
+ from hatch.mcp_host_config.models import (
14
+ MCPServerConfigKiro,
15
+ MCPServerConfigOmni,
16
+ MCPHostType
17
+ )
18
+
19
+
20
+ class TestMCPServerConfigKiro(unittest.TestCase):
21
+ """Test suite for MCPServerConfigKiro model validation."""
22
+
23
+ @regression_test
24
+ def test_kiro_model_with_disabled_field(self):
25
+ """Test Kiro model with disabled field."""
26
+ config = MCPServerConfigKiro(
27
+ name="kiro-server",
28
+ command="auggie",
29
+ args=["--mcp", "-m", "default"],
30
+ disabled=True
31
+ )
32
+
33
+ self.assertEqual(config.command, "auggie")
34
+ self.assertTrue(config.disabled)
35
+ self.assertEqual(config.type, "stdio") # Inferred
36
+
37
+ @regression_test
38
+ def test_kiro_model_with_auto_approve_tools(self):
39
+ """Test Kiro model with autoApprove field."""
40
+ config = MCPServerConfigKiro(
41
+ name="kiro-server",
42
+ command="auggie",
43
+ autoApprove=["codebase-retrieval", "fetch"]
44
+ )
45
+
46
+ self.assertEqual(config.command, "auggie")
47
+ self.assertEqual(len(config.autoApprove), 2)
48
+ self.assertIn("codebase-retrieval", config.autoApprove)
49
+ self.assertIn("fetch", config.autoApprove)
50
+
51
+ @regression_test
52
+ def test_kiro_model_with_disabled_tools(self):
53
+ """Test Kiro model with disabledTools field."""
54
+ config = MCPServerConfigKiro(
55
+ name="kiro-server",
56
+ command="python",
57
+ disabledTools=["dangerous-tool", "risky-tool"]
58
+ )
59
+
60
+ self.assertEqual(config.command, "python")
61
+ self.assertEqual(len(config.disabledTools), 2)
62
+ self.assertIn("dangerous-tool", config.disabledTools)
63
+
64
+ @regression_test
65
+ def test_kiro_model_all_fields_combined(self):
66
+ """Test Kiro model with all Kiro-specific fields."""
67
+ config = MCPServerConfigKiro(
68
+ name="kiro-server",
69
+ command="auggie",
70
+ args=["--mcp"],
71
+ env={"DEBUG": "true"},
72
+ disabled=False,
73
+ autoApprove=["codebase-retrieval"],
74
+ disabledTools=["dangerous-tool"]
75
+ )
76
+
77
+ # Verify all fields
78
+ self.assertEqual(config.command, "auggie")
79
+ self.assertFalse(config.disabled)
80
+ self.assertEqual(len(config.autoApprove), 1)
81
+ self.assertEqual(len(config.disabledTools), 1)
82
+ self.assertEqual(config.env["DEBUG"], "true")
83
+
84
+ @regression_test
85
+ def test_kiro_model_minimal_configuration(self):
86
+ """Test Kiro model with minimal configuration."""
87
+ config = MCPServerConfigKiro(
88
+ name="kiro-server",
89
+ command="auggie"
90
+ )
91
+
92
+ self.assertEqual(config.command, "auggie")
93
+ self.assertEqual(config.type, "stdio") # Inferred
94
+ self.assertIsNone(config.disabled)
95
+ self.assertIsNone(config.autoApprove)
96
+ self.assertIsNone(config.disabledTools)
97
+
98
+ @regression_test
99
+ def test_kiro_model_remote_server_with_kiro_fields(self):
100
+ """Test Kiro model with remote server and Kiro-specific fields."""
101
+ config = MCPServerConfigKiro(
102
+ name="kiro-remote",
103
+ url="https://api.example.com/mcp",
104
+ headers={"Authorization": "Bearer token"},
105
+ disabled=True,
106
+ autoApprove=["safe-tool"]
107
+ )
108
+
109
+ self.assertEqual(config.url, "https://api.example.com/mcp")
110
+ self.assertTrue(config.disabled)
111
+ self.assertEqual(len(config.autoApprove), 1)
112
+ self.assertEqual(config.type, "sse") # Inferred for remote
113
+
114
+
115
+ if __name__ == '__main__':
116
+ unittest.main()
@@ -0,0 +1,104 @@
1
+ """
2
+ Kiro MCP Omni Conversion Tests
3
+
4
+ Tests for conversion from MCPServerConfigOmni to MCPServerConfigKiro
5
+ using the from_omni() method.
6
+ """
7
+
8
+ import unittest
9
+
10
+ from wobble.decorators import regression_test
11
+
12
+ from hatch.mcp_host_config.models import (
13
+ MCPServerConfigKiro,
14
+ MCPServerConfigOmni
15
+ )
16
+
17
+
18
+ class TestKiroFromOmniConversion(unittest.TestCase):
19
+ """Test suite for Kiro from_omni() conversion method."""
20
+
21
+ @regression_test
22
+ def test_kiro_from_omni_with_supported_fields(self):
23
+ """Test Kiro from_omni with supported fields."""
24
+ omni = MCPServerConfigOmni(
25
+ name="kiro-server",
26
+ command="auggie",
27
+ args=["--mcp", "-m", "default"],
28
+ disabled=True,
29
+ autoApprove=["codebase-retrieval", "fetch"],
30
+ disabledTools=["dangerous-tool"]
31
+ )
32
+
33
+ # Convert to Kiro model
34
+ kiro = MCPServerConfigKiro.from_omni(omni)
35
+
36
+ # Verify all supported fields transferred
37
+ self.assertEqual(kiro.name, "kiro-server")
38
+ self.assertEqual(kiro.command, "auggie")
39
+ self.assertEqual(len(kiro.args), 3)
40
+ self.assertTrue(kiro.disabled)
41
+ self.assertEqual(len(kiro.autoApprove), 2)
42
+ self.assertEqual(len(kiro.disabledTools), 1)
43
+
44
+ @regression_test
45
+ def test_kiro_from_omni_with_unsupported_fields(self):
46
+ """Test Kiro from_omni excludes unsupported fields."""
47
+ omni = MCPServerConfigOmni(
48
+ name="kiro-server",
49
+ command="python",
50
+ disabled=True, # Kiro field
51
+ envFile=".env", # VS Code field (unsupported by Kiro)
52
+ timeout=30000 # Gemini field (unsupported by Kiro)
53
+ )
54
+
55
+ # Convert to Kiro model
56
+ kiro = MCPServerConfigKiro.from_omni(omni)
57
+
58
+ # Verify Kiro fields transferred
59
+ self.assertEqual(kiro.command, "python")
60
+ self.assertTrue(kiro.disabled)
61
+
62
+ # Verify unsupported fields NOT transferred
63
+ self.assertFalse(hasattr(kiro, 'envFile') and kiro.envFile is not None)
64
+ self.assertFalse(hasattr(kiro, 'timeout') and kiro.timeout is not None)
65
+
66
+ @regression_test
67
+ def test_kiro_from_omni_exclude_unset_behavior(self):
68
+ """Test that from_omni respects exclude_unset=True."""
69
+ omni = MCPServerConfigOmni(
70
+ name="kiro-server",
71
+ command="auggie"
72
+ # disabled, autoApprove, disabledTools not set
73
+ )
74
+
75
+ kiro = MCPServerConfigKiro.from_omni(omni)
76
+
77
+ # Verify unset fields remain None
78
+ self.assertIsNone(kiro.disabled)
79
+ self.assertIsNone(kiro.autoApprove)
80
+ self.assertIsNone(kiro.disabledTools)
81
+
82
+ @regression_test
83
+ def test_kiro_from_omni_remote_server_conversion(self):
84
+ """Test Kiro from_omni with remote server configuration."""
85
+ omni = MCPServerConfigOmni(
86
+ name="kiro-remote",
87
+ url="https://api.example.com/mcp",
88
+ headers={"Authorization": "Bearer token"},
89
+ disabled=False,
90
+ autoApprove=["safe-tool"]
91
+ )
92
+
93
+ kiro = MCPServerConfigKiro.from_omni(omni)
94
+
95
+ # Verify remote server fields
96
+ self.assertEqual(kiro.url, "https://api.example.com/mcp")
97
+ self.assertEqual(kiro.headers["Authorization"], "Bearer token")
98
+ self.assertFalse(kiro.disabled)
99
+ self.assertEqual(len(kiro.autoApprove), 1)
100
+ self.assertEqual(kiro.type, "sse") # Inferred for remote
101
+
102
+
103
+ if __name__ == '__main__':
104
+ unittest.main()
tests/test_data_utils.py CHANGED
@@ -292,6 +292,25 @@ class MCPHostConfigTestDataLoader(TestDataLoader):
292
292
  with open(config_path, 'r') as f:
293
293
  return json.load(f)
294
294
 
295
+ def load_kiro_mcp_config(self, config_type: str = "empty") -> Dict[str, Any]:
296
+ """Load Kiro-specific MCP configuration templates.
297
+
298
+ Args:
299
+ config_type: Type of Kiro configuration to load
300
+ - "empty": Empty mcpServers configuration
301
+ - "with_server": Single server with all Kiro fields
302
+ - "complex": Multi-server with mixed configurations
303
+
304
+ Returns:
305
+ Kiro MCP configuration dictionary
306
+ """
307
+ config_path = self.mcp_host_configs_dir / f"kiro_mcp_{config_type}.json"
308
+ if not config_path.exists():
309
+ self._create_kiro_mcp_config(config_type)
310
+
311
+ with open(config_path, 'r') as f:
312
+ return json.load(f)
313
+
295
314
  def _create_host_config_template(self, host_type: str, config_type: str):
296
315
  """Create host-specific configuration templates with inheritance patterns."""
297
316
  templates = {
@@ -364,6 +383,50 @@ class MCPHostConfigTestDataLoader(TestDataLoader):
364
383
  "args": ["server.py"]
365
384
  }
366
385
  }
386
+ },
387
+
388
+ # Kiro family templates
389
+ "kiro_simple": {
390
+ "mcpServers": {
391
+ "test_server": {
392
+ "command": "auggie",
393
+ "args": ["--mcp"],
394
+ "disabled": False,
395
+ "autoApprove": ["codebase-retrieval"]
396
+ }
397
+ }
398
+ },
399
+ "kiro_with_server": {
400
+ "mcpServers": {
401
+ "existing-server": {
402
+ "command": "auggie",
403
+ "args": ["--mcp", "-m", "default", "-w", "."],
404
+ "env": {"DEBUG": "true"},
405
+ "disabled": False,
406
+ "autoApprove": ["codebase-retrieval", "fetch"],
407
+ "disabledTools": ["dangerous-tool"]
408
+ }
409
+ }
410
+ },
411
+ "kiro_complex": {
412
+ "mcpServers": {
413
+ "local-server": {
414
+ "command": "auggie",
415
+ "args": ["--mcp"],
416
+ "disabled": False,
417
+ "autoApprove": ["codebase-retrieval"]
418
+ },
419
+ "remote-server": {
420
+ "url": "https://api.example.com/mcp",
421
+ "headers": {"Authorization": "Bearer token"},
422
+ "disabled": True,
423
+ "disabledTools": ["risky-tool"]
424
+ }
425
+ },
426
+ "otherSettings": {
427
+ "theme": "dark",
428
+ "fontSize": 14
429
+ }
367
430
  }
368
431
  }
369
432
 
@@ -470,3 +533,48 @@ class MCPHostConfigTestDataLoader(TestDataLoader):
470
533
  config_path = self.mcp_host_configs_dir / f"mcp_server_{server_type}.json"
471
534
  with open(config_path, 'w') as f:
472
535
  json.dump(config, f, indent=2)
536
+
537
+ def _create_kiro_mcp_config(self, config_type: str):
538
+ """Create Kiro-specific MCP configuration templates."""
539
+ templates = {
540
+ "empty": {
541
+ "mcpServers": {}
542
+ },
543
+ "with_server": {
544
+ "mcpServers": {
545
+ "existing-server": {
546
+ "command": "auggie",
547
+ "args": ["--mcp", "-m", "default", "-w", "."],
548
+ "env": {"DEBUG": "true"},
549
+ "disabled": False,
550
+ "autoApprove": ["codebase-retrieval", "fetch"],
551
+ "disabledTools": ["dangerous-tool"]
552
+ }
553
+ }
554
+ },
555
+ "complex": {
556
+ "mcpServers": {
557
+ "local-server": {
558
+ "command": "auggie",
559
+ "args": ["--mcp"],
560
+ "disabled": False,
561
+ "autoApprove": ["codebase-retrieval"]
562
+ },
563
+ "remote-server": {
564
+ "url": "https://api.example.com/mcp",
565
+ "headers": {"Authorization": "Bearer token"},
566
+ "disabled": True,
567
+ "disabledTools": ["risky-tool"]
568
+ }
569
+ },
570
+ "otherSettings": {
571
+ "theme": "dark",
572
+ "fontSize": 14
573
+ }
574
+ }
575
+ }
576
+
577
+ config = templates.get(config_type, {"mcpServers": {}})
578
+ config_path = self.mcp_host_configs_dir / f"kiro_mcp_{config_type}.json"
579
+ with open(config_path, 'w') as f:
580
+ json.dump(config, f, indent=2)
@@ -15,7 +15,7 @@ from hatch.cli_hatch import handle_mcp_configure, parse_input
15
15
  from hatch.mcp_host_config import MCPHostType
16
16
  from hatch.mcp_host_config.models import (
17
17
  MCPServerConfigGemini, MCPServerConfigCursor, MCPServerConfigVSCode,
18
- MCPServerConfigClaude
18
+ MCPServerConfigClaude, MCPServerConfigCodex
19
19
  )
20
20
 
21
21
 
@@ -298,6 +298,199 @@ class TestToolFilteringArguments(unittest.TestCase):
298
298
  self.assertEqual(server_config.excludeTools, ['dangerous_tool'])
299
299
 
300
300
 
301
+ class TestAllCodexArguments(unittest.TestCase):
302
+ """Test ALL Codex-specific CLI arguments."""
303
+
304
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
305
+ @patch('sys.stdout', new_callable=StringIO)
306
+ def test_all_codex_arguments_accepted(self, mock_stdout, mock_manager_class):
307
+ """Test that all Codex arguments are accepted and passed to model."""
308
+ mock_manager = MagicMock()
309
+ mock_manager_class.return_value = mock_manager
310
+
311
+ mock_result = MagicMock()
312
+ mock_result.success = True
313
+ mock_result.backup_path = None
314
+ mock_manager.configure_server.return_value = mock_result
315
+
316
+ # Test STDIO server with Codex-specific STDIO fields
317
+ result = handle_mcp_configure(
318
+ host='codex',
319
+ server_name='test-server',
320
+ command='npx',
321
+ args=['-y', '@upstash/context7-mcp'],
322
+ env_vars=['PATH', 'HOME'],
323
+ cwd='/workspace',
324
+ startup_timeout=15,
325
+ tool_timeout=120,
326
+ enabled=True,
327
+ include_tools=['read', 'write'],
328
+ exclude_tools=['delete'],
329
+ auto_approve=True
330
+ )
331
+
332
+ # Verify success
333
+ self.assertEqual(result, 0)
334
+
335
+ # Verify configure_server was called
336
+ mock_manager.configure_server.assert_called_once()
337
+
338
+ # Verify server_config is MCPServerConfigCodex
339
+ call_args = mock_manager.configure_server.call_args
340
+ server_config = call_args.kwargs['server_config']
341
+ self.assertIsInstance(server_config, MCPServerConfigCodex)
342
+
343
+ # Verify Codex-specific STDIO fields
344
+ self.assertEqual(server_config.env_vars, ['PATH', 'HOME'])
345
+ self.assertEqual(server_config.cwd, '/workspace')
346
+ self.assertEqual(server_config.startup_timeout_sec, 15)
347
+ self.assertEqual(server_config.tool_timeout_sec, 120)
348
+ self.assertTrue(server_config.enabled)
349
+ self.assertEqual(server_config.enabled_tools, ['read', 'write'])
350
+ self.assertEqual(server_config.disabled_tools, ['delete'])
351
+
352
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
353
+ @patch('sys.stdout', new_callable=StringIO)
354
+ def test_codex_env_vars_list(self, mock_stdout, mock_manager_class):
355
+ """Test that env_vars accepts multiple values as a list."""
356
+ mock_manager = MagicMock()
357
+ mock_manager_class.return_value = mock_manager
358
+
359
+ mock_result = MagicMock()
360
+ mock_result.success = True
361
+ mock_result.backup_path = None
362
+ mock_manager.configure_server.return_value = mock_result
363
+
364
+ result = handle_mcp_configure(
365
+ host='codex',
366
+ server_name='test-server',
367
+ command='npx',
368
+ args=['-y', 'package'],
369
+ env_vars=['PATH', 'HOME', 'USER'],
370
+ auto_approve=True
371
+ )
372
+
373
+ self.assertEqual(result, 0)
374
+ call_args = mock_manager.configure_server.call_args
375
+ server_config = call_args.kwargs['server_config']
376
+ self.assertEqual(server_config.env_vars, ['PATH', 'HOME', 'USER'])
377
+
378
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
379
+ @patch('sys.stdout', new_callable=StringIO)
380
+ def test_codex_env_header_parsing(self, mock_stdout, mock_manager_class):
381
+ """Test that env_header parses KEY=ENV_VAR format correctly."""
382
+ mock_manager = MagicMock()
383
+ mock_manager_class.return_value = mock_manager
384
+
385
+ mock_result = MagicMock()
386
+ mock_result.success = True
387
+ mock_result.backup_path = None
388
+ mock_manager.configure_server.return_value = mock_result
389
+
390
+ result = handle_mcp_configure(
391
+ host='codex',
392
+ server_name='test-server',
393
+ command='npx',
394
+ args=['-y', 'package'],
395
+ env_header=['X-API-Key=API_KEY', 'Authorization=AUTH_TOKEN'],
396
+ auto_approve=True
397
+ )
398
+
399
+ self.assertEqual(result, 0)
400
+ call_args = mock_manager.configure_server.call_args
401
+ server_config = call_args.kwargs['server_config']
402
+ self.assertEqual(server_config.env_http_headers, {
403
+ 'X-API-Key': 'API_KEY',
404
+ 'Authorization': 'AUTH_TOKEN'
405
+ })
406
+
407
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
408
+ @patch('sys.stdout', new_callable=StringIO)
409
+ def test_codex_timeout_fields(self, mock_stdout, mock_manager_class):
410
+ """Test that timeout fields are passed as integers."""
411
+ mock_manager = MagicMock()
412
+ mock_manager_class.return_value = mock_manager
413
+
414
+ mock_result = MagicMock()
415
+ mock_result.success = True
416
+ mock_result.backup_path = None
417
+ mock_manager.configure_server.return_value = mock_result
418
+
419
+ result = handle_mcp_configure(
420
+ host='codex',
421
+ server_name='test-server',
422
+ command='npx',
423
+ args=['-y', 'package'],
424
+ startup_timeout=30,
425
+ tool_timeout=180,
426
+ auto_approve=True
427
+ )
428
+
429
+ self.assertEqual(result, 0)
430
+ call_args = mock_manager.configure_server.call_args
431
+ server_config = call_args.kwargs['server_config']
432
+ self.assertEqual(server_config.startup_timeout_sec, 30)
433
+ self.assertEqual(server_config.tool_timeout_sec, 180)
434
+
435
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
436
+ @patch('sys.stdout', new_callable=StringIO)
437
+ def test_codex_enabled_flag(self, mock_stdout, mock_manager_class):
438
+ """Test that enabled flag works as boolean."""
439
+ mock_manager = MagicMock()
440
+ mock_manager_class.return_value = mock_manager
441
+
442
+ mock_result = MagicMock()
443
+ mock_result.success = True
444
+ mock_result.backup_path = None
445
+ mock_manager.configure_server.return_value = mock_result
446
+
447
+ result = handle_mcp_configure(
448
+ host='codex',
449
+ server_name='test-server',
450
+ command='npx',
451
+ args=['-y', 'package'],
452
+ enabled=True,
453
+ auto_approve=True
454
+ )
455
+
456
+ self.assertEqual(result, 0)
457
+ call_args = mock_manager.configure_server.call_args
458
+ server_config = call_args.kwargs['server_config']
459
+ self.assertTrue(server_config.enabled)
460
+
461
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
462
+ @patch('sys.stdout', new_callable=StringIO)
463
+ def test_codex_reuses_shared_arguments(self, mock_stdout, mock_manager_class):
464
+ """Test that Codex reuses shared arguments (cwd, include-tools, exclude-tools)."""
465
+ mock_manager = MagicMock()
466
+ mock_manager_class.return_value = mock_manager
467
+
468
+ mock_result = MagicMock()
469
+ mock_result.success = True
470
+ mock_result.backup_path = None
471
+ mock_manager.configure_server.return_value = mock_result
472
+
473
+ result = handle_mcp_configure(
474
+ host='codex',
475
+ server_name='test-server',
476
+ command='npx',
477
+ args=['-y', 'package'],
478
+ cwd='/workspace',
479
+ include_tools=['tool1', 'tool2'],
480
+ exclude_tools=['tool3'],
481
+ auto_approve=True
482
+ )
483
+
484
+ self.assertEqual(result, 0)
485
+ call_args = mock_manager.configure_server.call_args
486
+ server_config = call_args.kwargs['server_config']
487
+
488
+ # Verify shared arguments work for Codex STDIO servers
489
+ self.assertEqual(server_config.cwd, '/workspace')
490
+ self.assertEqual(server_config.enabled_tools, ['tool1', 'tool2'])
491
+ self.assertEqual(server_config.disabled_tools, ['tool3'])
492
+
493
+
301
494
  if __name__ == '__main__':
302
495
  unittest.main()
303
496
 
@@ -40,17 +40,19 @@ class TestMCPConfigureCommand(unittest.TestCase):
40
40
  try:
41
41
  result = main()
42
42
  # If main() returns without SystemExit, check the handler was called
43
- # Updated to include ALL host-specific parameters
43
+ # Updated to include ALL host-specific parameters (27 total)
44
44
  mock_handler.assert_called_once_with(
45
45
  'claude-desktop', 'weather-server', 'python', ['weather.py'],
46
- None, None, None, None, False, None, None, None, None, None, None, False, False, False
46
+ None, None, None, None, False, None, None, None, None, None, None,
47
+ False, None, None, None, None, None, False, None, None, False, False, False
47
48
  )
48
49
  except SystemExit as e:
49
50
  # If SystemExit is raised, it should be 0 (success) and handler should have been called
50
51
  if e.code == 0:
51
52
  mock_handler.assert_called_once_with(
52
53
  'claude-desktop', 'weather-server', 'python', ['weather.py'],
53
- None, None, None, None, False, None, None, None, None, None, None, False, False, False
54
+ None, None, None, None, False, None, None, None, None, None, None,
55
+ False, None, None, None, None, None, False, None, None, False, False, False
54
56
  )
55
57
  else:
56
58
  self.fail(f"main() exited with code {e.code}, expected 0")
@@ -70,11 +72,12 @@ class TestMCPConfigureCommand(unittest.TestCase):
70
72
  with patch('hatch.cli_hatch.handle_mcp_configure', return_value=0) as mock_handler:
71
73
  try:
72
74
  main()
73
- # Updated to include ALL host-specific parameters
75
+ # Updated to include ALL host-specific parameters (27 total)
74
76
  mock_handler.assert_called_once_with(
75
77
  'cursor', 'file-server', None, None,
76
78
  ['API_KEY=secret', 'DEBUG=true'], 'http://localhost:8080',
77
- ['Authorization=Bearer token'], None, False, None, None, None, None, None, None, True, True, True
79
+ ['Authorization=Bearer token'], None, False, None, None, None, None, None, None,
80
+ False, None, None, None, None, None, False, None, None, True, True, True
78
81
  )
79
82
  except SystemExit as e:
80
83
  self.assertEqual(e.code, 0)