hatch-xclam 0.7.1.dev1__py3-none-any.whl → 0.7.1.dev3__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/cli_hatch.py +92 -21
- hatch/mcp_host_config/__init__.py +4 -2
- hatch/mcp_host_config/backup.py +62 -31
- hatch/mcp_host_config/models.py +98 -5
- hatch/mcp_host_config/strategies.py +172 -1
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev3.dist-info}/METADATA +5 -3
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev3.dist-info}/RECORD +16 -13
- tests/regression/test_mcp_codex_backup_integration.py +162 -0
- tests/regression/test_mcp_codex_host_strategy.py +163 -0
- tests/regression/test_mcp_codex_model_validation.py +117 -0
- tests/test_mcp_cli_all_host_specific_args.py +194 -1
- tests/test_mcp_cli_direct_management.py +8 -5
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev3.dist-info}/WHEEL +0 -0
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev3.dist-info}/entry_points.txt +0 -0
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev3.dist-info}/licenses/LICENSE +0 -0
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev3.dist-info}/top_level.txt +0 -0
|
@@ -8,8 +8,10 @@ strategies with decorator registration following Hatchling patterns.
|
|
|
8
8
|
|
|
9
9
|
import platform
|
|
10
10
|
import json
|
|
11
|
+
import tomllib # Python 3.11+ built-in
|
|
12
|
+
import tomli_w # TOML writing
|
|
11
13
|
from pathlib import Path
|
|
12
|
-
from typing import Optional, Dict, Any
|
|
14
|
+
from typing import Optional, Dict, Any, TextIO
|
|
13
15
|
import logging
|
|
14
16
|
|
|
15
17
|
from .host_management import MCPHostStrategy, register_host_strategy
|
|
@@ -607,3 +609,172 @@ class GeminiHostStrategy(MCPHostStrategy):
|
|
|
607
609
|
except Exception as e:
|
|
608
610
|
logger.error(f"Failed to write Gemini configuration: {e}")
|
|
609
611
|
return False
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@register_host_strategy(MCPHostType.CODEX)
|
|
615
|
+
class CodexHostStrategy(MCPHostStrategy):
|
|
616
|
+
"""Configuration strategy for Codex IDE with TOML support.
|
|
617
|
+
|
|
618
|
+
Codex uses TOML configuration at ~/.codex/config.toml with a unique
|
|
619
|
+
structure using [mcp_servers.<server-name>] tables.
|
|
620
|
+
"""
|
|
621
|
+
|
|
622
|
+
def __init__(self):
|
|
623
|
+
self.config_format = "toml"
|
|
624
|
+
self._preserved_features = {} # Preserve [features] section
|
|
625
|
+
|
|
626
|
+
def get_config_path(self) -> Optional[Path]:
|
|
627
|
+
"""Get Codex configuration path."""
|
|
628
|
+
return Path.home() / ".codex" / "config.toml"
|
|
629
|
+
|
|
630
|
+
def get_config_key(self) -> str:
|
|
631
|
+
"""Codex uses 'mcp_servers' key (note: underscore, not camelCase)."""
|
|
632
|
+
return "mcp_servers"
|
|
633
|
+
|
|
634
|
+
def is_host_available(self) -> bool:
|
|
635
|
+
"""Check if Codex is available by checking for config directory."""
|
|
636
|
+
codex_dir = Path.home() / ".codex"
|
|
637
|
+
return codex_dir.exists()
|
|
638
|
+
|
|
639
|
+
def validate_server_config(self, server_config: MCPServerConfig) -> bool:
|
|
640
|
+
"""Codex validation - supports both STDIO and HTTP servers."""
|
|
641
|
+
return server_config.command is not None or server_config.url is not None
|
|
642
|
+
|
|
643
|
+
def read_configuration(self) -> HostConfiguration:
|
|
644
|
+
"""Read Codex TOML configuration file."""
|
|
645
|
+
config_path = self.get_config_path()
|
|
646
|
+
if not config_path or not config_path.exists():
|
|
647
|
+
return HostConfiguration(servers={})
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
with open(config_path, 'rb') as f:
|
|
651
|
+
toml_data = tomllib.load(f)
|
|
652
|
+
|
|
653
|
+
# Preserve [features] section for later write
|
|
654
|
+
self._preserved_features = toml_data.get('features', {})
|
|
655
|
+
|
|
656
|
+
# Extract MCP servers from [mcp_servers.*] tables
|
|
657
|
+
mcp_servers = toml_data.get(self.get_config_key(), {})
|
|
658
|
+
|
|
659
|
+
servers = {}
|
|
660
|
+
for name, server_data in mcp_servers.items():
|
|
661
|
+
try:
|
|
662
|
+
# Flatten nested env section if present
|
|
663
|
+
flat_data = self._flatten_toml_server(server_data)
|
|
664
|
+
servers[name] = MCPServerConfig(**flat_data)
|
|
665
|
+
except Exception as e:
|
|
666
|
+
logger.warning(f"Invalid server config for {name}: {e}")
|
|
667
|
+
continue
|
|
668
|
+
|
|
669
|
+
return HostConfiguration(servers=servers)
|
|
670
|
+
|
|
671
|
+
except Exception as e:
|
|
672
|
+
logger.error(f"Failed to read Codex configuration: {e}")
|
|
673
|
+
return HostConfiguration(servers={})
|
|
674
|
+
|
|
675
|
+
def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
|
|
676
|
+
"""Write Codex TOML configuration file with backup support."""
|
|
677
|
+
config_path = self.get_config_path()
|
|
678
|
+
if not config_path:
|
|
679
|
+
return False
|
|
680
|
+
|
|
681
|
+
try:
|
|
682
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
683
|
+
|
|
684
|
+
# Read existing configuration to preserve non-MCP settings
|
|
685
|
+
existing_data = {}
|
|
686
|
+
if config_path.exists():
|
|
687
|
+
try:
|
|
688
|
+
with open(config_path, 'rb') as f:
|
|
689
|
+
existing_data = tomllib.load(f)
|
|
690
|
+
except Exception:
|
|
691
|
+
pass
|
|
692
|
+
|
|
693
|
+
# Preserve [features] section
|
|
694
|
+
if 'features' in existing_data:
|
|
695
|
+
self._preserved_features = existing_data['features']
|
|
696
|
+
|
|
697
|
+
# Convert servers to TOML structure
|
|
698
|
+
servers_data = {}
|
|
699
|
+
for name, server_config in config.servers.items():
|
|
700
|
+
servers_data[name] = self._to_toml_server(server_config)
|
|
701
|
+
|
|
702
|
+
# Build final TOML structure
|
|
703
|
+
final_data = {}
|
|
704
|
+
|
|
705
|
+
# Preserve [features] at top
|
|
706
|
+
if self._preserved_features:
|
|
707
|
+
final_data['features'] = self._preserved_features
|
|
708
|
+
|
|
709
|
+
# Add MCP servers
|
|
710
|
+
final_data[self.get_config_key()] = servers_data
|
|
711
|
+
|
|
712
|
+
# Preserve other top-level keys
|
|
713
|
+
for key, value in existing_data.items():
|
|
714
|
+
if key not in ('features', self.get_config_key()):
|
|
715
|
+
final_data[key] = value
|
|
716
|
+
|
|
717
|
+
# Use atomic write with TOML serializer
|
|
718
|
+
backup_manager = MCPHostConfigBackupManager()
|
|
719
|
+
atomic_ops = AtomicFileOperations()
|
|
720
|
+
|
|
721
|
+
def toml_serializer(data: Any, f: TextIO) -> None:
|
|
722
|
+
# tomli_w.dumps returns a string, write it to the file
|
|
723
|
+
toml_str = tomli_w.dumps(data)
|
|
724
|
+
f.write(toml_str)
|
|
725
|
+
|
|
726
|
+
atomic_ops.atomic_write_with_serializer(
|
|
727
|
+
file_path=config_path,
|
|
728
|
+
data=final_data,
|
|
729
|
+
serializer=toml_serializer,
|
|
730
|
+
backup_manager=backup_manager,
|
|
731
|
+
hostname="codex",
|
|
732
|
+
skip_backup=no_backup
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
return True
|
|
736
|
+
|
|
737
|
+
except Exception as e:
|
|
738
|
+
logger.error(f"Failed to write Codex configuration: {e}")
|
|
739
|
+
return False
|
|
740
|
+
|
|
741
|
+
def _flatten_toml_server(self, server_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
742
|
+
"""Flatten nested TOML server structure to flat dict.
|
|
743
|
+
|
|
744
|
+
TOML structure:
|
|
745
|
+
[mcp_servers.name]
|
|
746
|
+
command = "npx"
|
|
747
|
+
args = ["-y", "package"]
|
|
748
|
+
[mcp_servers.name.env]
|
|
749
|
+
VAR = "value"
|
|
750
|
+
|
|
751
|
+
Becomes:
|
|
752
|
+
{"command": "npx", "args": [...], "env": {"VAR": "value"}}
|
|
753
|
+
|
|
754
|
+
Also maps Codex-specific 'http_headers' to universal 'headers' field.
|
|
755
|
+
"""
|
|
756
|
+
# TOML already parses nested tables into nested dicts
|
|
757
|
+
# So [mcp_servers.name.env] becomes {"env": {...}}
|
|
758
|
+
data = dict(server_data)
|
|
759
|
+
|
|
760
|
+
# Map Codex 'http_headers' to universal 'headers' for MCPServerConfig
|
|
761
|
+
if 'http_headers' in data:
|
|
762
|
+
data['headers'] = data.pop('http_headers')
|
|
763
|
+
|
|
764
|
+
return data
|
|
765
|
+
|
|
766
|
+
def _to_toml_server(self, server_config: MCPServerConfig) -> Dict[str, Any]:
|
|
767
|
+
"""Convert MCPServerConfig to TOML-compatible dict structure.
|
|
768
|
+
|
|
769
|
+
Maps universal 'headers' field back to Codex-specific 'http_headers'.
|
|
770
|
+
"""
|
|
771
|
+
data = server_config.model_dump(exclude_unset=True)
|
|
772
|
+
|
|
773
|
+
# Remove 'name' field as it's the table key in TOML
|
|
774
|
+
data.pop('name', None)
|
|
775
|
+
|
|
776
|
+
# Map universal 'headers' to Codex 'http_headers' for TOML
|
|
777
|
+
if 'headers' in data:
|
|
778
|
+
data['http_headers'] = data.pop('headers')
|
|
779
|
+
|
|
780
|
+
return data
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hatch-xclam
|
|
3
|
-
Version: 0.7.1.
|
|
3
|
+
Version: 0.7.1.dev3
|
|
4
4
|
Summary: Package manager for the Cracking Shells ecosystem
|
|
5
5
|
Author: Cracking Shells Team
|
|
6
6
|
Project-URL: Homepage, https://github.com/CrackingShells/Hatch
|
|
@@ -17,6 +17,7 @@ Requires-Dist: packaging>=20.0
|
|
|
17
17
|
Requires-Dist: docker>=7.1.0
|
|
18
18
|
Requires-Dist: pydantic>=2.0.0
|
|
19
19
|
Requires-Dist: hatch-validator>=0.8.0
|
|
20
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
20
21
|
Provides-Extra: docs
|
|
21
22
|
Requires-Dist: mkdocs>=1.4.0; extra == "docs"
|
|
22
23
|
Requires-Dist: mkdocstrings[python]>=0.20.0; extra == "docs"
|
|
@@ -30,7 +31,7 @@ Dynamic: license-file
|
|
|
30
31
|
|
|
31
32
|
## Introduction
|
|
32
33
|
|
|
33
|
-
Hatch is the package manager for managing Model Context Protocol (MCP) servers with environment isolation, multi-type dependency resolution, and multi-host deployment. Deploy MCP servers to Claude Desktop, VS Code, Cursor, Kiro, and other platforms with automatic dependency management.
|
|
34
|
+
Hatch is the package manager for managing Model Context Protocol (MCP) servers with environment isolation, multi-type dependency resolution, and multi-host deployment. Deploy MCP servers to Claude Desktop, VS Code, Cursor, Kiro, Codex, and other platforms with automatic dependency management.
|
|
34
35
|
|
|
35
36
|
The canonical documentation is at `docs/index.md` and published at <https://hatch.readthedocs.io/en/latest/>.
|
|
36
37
|
|
|
@@ -38,7 +39,7 @@ The canonical documentation is at `docs/index.md` and published at <https://hatc
|
|
|
38
39
|
|
|
39
40
|
- **Environment Isolation** — Create separate, isolated workspaces for different projects without conflicts
|
|
40
41
|
- **Multi-Type Dependency Resolution** — Automatically resolve and install system packages, Python packages, Docker containers, and Hatch packages
|
|
41
|
-
- **Multi-Host Deployment** —
|
|
42
|
+
- **Multi-Host Deployment** — Configure MCP servers on multiple host platforms
|
|
42
43
|
- **Package Validation** — Ensure packages meet schema requirements before distribution
|
|
43
44
|
- **Development-Focused** — Optimized for rapid development and testing of MCP server ecosystems
|
|
44
45
|
|
|
@@ -51,6 +52,7 @@ Hatch supports deployment to the following MCP host platforms:
|
|
|
51
52
|
- **VS Code** — Visual Studio Code with the MCP extension for tool integration
|
|
52
53
|
- **Cursor** — AI-first code editor with built-in MCP server support
|
|
53
54
|
- **Kiro** — Kiro IDE with MCP support
|
|
55
|
+
- **Codex** — OpenAI Codex with MCP server configuration support
|
|
54
56
|
- **LM Studio** — Local LLM inference platform with MCP server integration
|
|
55
57
|
- **Google Gemini CLI** — Command-line interface for Google's Gemini model with MCP support
|
|
56
58
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
hatch/__init__.py,sha256=5JFQZiaZQewEWg8WktQKEdT8IeH0KstndZf27VH7sq4,594
|
|
2
|
-
hatch/cli_hatch.py,sha256=
|
|
2
|
+
hatch/cli_hatch.py,sha256=Z7-N0DdWT1xK23EL0Gr5mx2wjqhDFzFROt1nUxPAnZM,112093
|
|
3
3
|
hatch/environment_manager.py,sha256=9R9PJYPKQLmWeGXBrOzXxty20la33LgCCYY8o2aMFBQ,60757
|
|
4
4
|
hatch/package_loader.py,sha256=Sa2JIoio1QlMT2tOGwZhC6pFJIs419cYyoodzyaTDl4,11269
|
|
5
5
|
hatch/python_environment_manager.py,sha256=guU3zz4_WG3ptuX_ATGCRIi_fDxNHlaQtMv3kiRSo8k,28894
|
|
@@ -15,13 +15,13 @@ hatch/installers/installer_base.py,sha256=mId6Q_DLOQPZriq3wu3BCU-ckouom3EZgbWJQq
|
|
|
15
15
|
hatch/installers/python_installer.py,sha256=MS9Q8wKjMAy7MEWk7zcAAiFgN0KzOVJFmMzXt1MSH8g,13632
|
|
16
16
|
hatch/installers/registry.py,sha256=ZOEEMJy_kL5LVj5Mf7s1_CIovDnUVag6nB01dEU9Xeg,6831
|
|
17
17
|
hatch/installers/system_installer.py,sha256=bdrmw3I9g2EU2E94-4vtJj01RhmekX9GxylU1RPT3Lk,22869
|
|
18
|
-
hatch/mcp_host_config/__init__.py,sha256=
|
|
19
|
-
hatch/mcp_host_config/backup.py,sha256=
|
|
18
|
+
hatch/mcp_host_config/__init__.py,sha256=STHzYwcyO6blKSwcMRibcD_4VKHgpEjru-uY1OQM9yA,1781
|
|
19
|
+
hatch/mcp_host_config/backup.py,sha256=X6wnLkPFYv4e5ObrWQSgQ_a_6rcmsFgysQwBZF_osWM,17831
|
|
20
20
|
hatch/mcp_host_config/host_management.py,sha256=sXyGluFQpfXKggxAVvV9riGRis29JnoEM2dTWSIwb24,23905
|
|
21
|
-
hatch/mcp_host_config/models.py,sha256=
|
|
21
|
+
hatch/mcp_host_config/models.py,sha256=1Nd3PDyGGbBm4oTgMEQ67j4b_qy23iibv9N4Tm4d9oE,29110
|
|
22
22
|
hatch/mcp_host_config/reporting.py,sha256=Q8UKBJRfvJTbb5PM9xwLEOh3OJjf19AKpWKxs-2622k,6889
|
|
23
|
-
hatch/mcp_host_config/strategies.py,sha256=
|
|
24
|
-
hatch_xclam-0.7.1.
|
|
23
|
+
hatch/mcp_host_config/strategies.py,sha256=NdA8hcbAi5xGkFRy51csmdEJESg25L8JkwTDVg2AeMw,30302
|
|
24
|
+
hatch_xclam-0.7.1.dev3.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
25
25
|
tests/__init__.py,sha256=4I3aQWv143Y1QY_nRIBWnY9MIL-aoQOJuVlpoPQz24E,53
|
|
26
26
|
tests/run_environment_tests.py,sha256=bWCr8UsPgU80SM8f_VSi0TCwDI6JNqZpvZ2W9-b2Lqk,7302
|
|
27
27
|
tests/test_cli_version.py,sha256=lU8TBZfzn_8AenFNXYrLMARht91fI5twBN13L-iJebc,4778
|
|
@@ -33,9 +33,9 @@ tests/test_hatch_installer.py,sha256=qHCEcKpbe0fHvkzib2rcUJUw1Z-PCRBBJhbjoyOYiVM
|
|
|
33
33
|
tests/test_installer_base.py,sha256=0xZiPDMf8LeFJs2SmnYhbIgL7mgvG_HKwL1myQovEZI,11818
|
|
34
34
|
tests/test_mcp_atomic_operations.py,sha256=QmwUDRNZUz6b2i50yRAMBntRDaMMYZt6flHFJfVkzNE,10563
|
|
35
35
|
tests/test_mcp_backup_integration.py,sha256=Auw6Bx1EXGwmCA-mRIy31DRLuRWyV33eB7GJiOvIPXQ,12360
|
|
36
|
-
tests/test_mcp_cli_all_host_specific_args.py,sha256=
|
|
36
|
+
tests/test_mcp_cli_all_host_specific_args.py,sha256=hvXUetFyoZFtHsEjpzZ0GNdEuvci3Hv8P8thVjBPm-A,18748
|
|
37
37
|
tests/test_mcp_cli_backup_management.py,sha256=GlUUNu5K1w8S2jTQ6YISp5KeXH5c-H9F0lJX2SG0JWM,14019
|
|
38
|
-
tests/test_mcp_cli_direct_management.py,sha256=
|
|
38
|
+
tests/test_mcp_cli_direct_management.py,sha256=7GRwHXezmImCUfpHhVjvSkGfT-OBi1tMMLgExMXjals,22184
|
|
39
39
|
tests/test_mcp_cli_discovery_listing.py,sha256=kdrCU6POLyGW9ejowNV-dUVDFVseMd_vibvgIDjZUCM,26595
|
|
40
40
|
tests/test_mcp_cli_host_config_integration.py,sha256=dD6maHP0wHWnFZwxJ5LgSK1GsrYqB4WdicZkotqIANo,32512
|
|
41
41
|
tests/test_mcp_cli_package_management.py,sha256=YFMhyh3dueel1f2R5_VMNr9AewDmVrqGbU1kj5bhdeo,14590
|
|
@@ -59,6 +59,9 @@ tests/test_system_installer.py,sha256=bWuyEKakhvi51iM8xHJh62zv83HUTd8QnPlqUUMWx9
|
|
|
59
59
|
tests/integration/__init__.py,sha256=2mG53dv1VqjxZYHuglneK9VgDMoWCxpfmPByXXd3zVM,125
|
|
60
60
|
tests/integration/test_mcp_kiro_integration.py,sha256=9y2XPacd3Y6zkqUUcCDjzj8Jv2Humby2LZ5CLSeFYCg,5566
|
|
61
61
|
tests/regression/__init__.py,sha256=0pFnFuEaMf7gPFFXMv-b_vNRNyLV-wU2lYspZHFH_Uo,127
|
|
62
|
+
tests/regression/test_mcp_codex_backup_integration.py,sha256=15G8tCPFtukEcZvE5NBI8BvxlD27KwmE3z8-8ZZ3GxM,7107
|
|
63
|
+
tests/regression/test_mcp_codex_host_strategy.py,sha256=6ehG8IruT9U-fO7kDMoJfogXbpq7JV4XYl-Ho68nt1U,6413
|
|
64
|
+
tests/regression/test_mcp_codex_model_validation.py,sha256=LkGexJrDusxRg_5Bpsm7sXRt7LYwhqqj4Y7VIynslyw,4512
|
|
62
65
|
tests/regression/test_mcp_kiro_backup_integration.py,sha256=oBEnSSLrnHIurkrBtjSG-HT6DyODV2z9tZeZotnsR1k,9584
|
|
63
66
|
tests/regression/test_mcp_kiro_cli_integration.py,sha256=jDQE73yJTRb2e6MPShxLHmLDtN5VBMHMAUBojPEnVBI,5123
|
|
64
67
|
tests/regression/test_mcp_kiro_decorator_registration.py,sha256=_H9FdKdKCv__IYBS0tfnZGUP574KoDjqKInmFiDEKPc,2556
|
|
@@ -95,8 +98,8 @@ tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py,sha256=_B5aqX
|
|
|
95
98
|
tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py,sha256=rinhVySJpjXKd2sRCS0ps7xTrVqImWcZ8l4aYbidYR8,238
|
|
96
99
|
tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py,sha256=FT14llzHlA4i8I__8GugzBRowhg_CbLmsOwjq0IWFsY,368
|
|
97
100
|
tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py,sha256=BRPAyyAseE2CGR3W647SwjlluYfi7ejhZck0An5581I,467
|
|
98
|
-
hatch_xclam-0.7.1.
|
|
99
|
-
hatch_xclam-0.7.1.
|
|
100
|
-
hatch_xclam-0.7.1.
|
|
101
|
-
hatch_xclam-0.7.1.
|
|
102
|
-
hatch_xclam-0.7.1.
|
|
101
|
+
hatch_xclam-0.7.1.dev3.dist-info/METADATA,sha256=-QwerIq3PVxHdoIVq32lneCmCgh2kQBHm9N1jrIAIdY,5947
|
|
102
|
+
hatch_xclam-0.7.1.dev3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
103
|
+
hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt,sha256=6xbkwFUtr7nRa56vUFMyJk2wjwFQ_XVaU53ruecWKI0,47
|
|
104
|
+
hatch_xclam-0.7.1.dev3.dist-info/top_level.txt,sha256=GZP3Ivciwal8jVITQkQr7dSNlLJRzfNOhA76VN7Jp4Y,12
|
|
105
|
+
hatch_xclam-0.7.1.dev3.dist-info/RECORD,,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codex MCP Backup Integration Tests
|
|
3
|
+
|
|
4
|
+
Tests for Codex TOML backup integration including backup creation,
|
|
5
|
+
restoration, and the no_backup parameter.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
import tempfile
|
|
10
|
+
import tomllib
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from wobble.decorators import regression_test
|
|
14
|
+
|
|
15
|
+
from hatch.mcp_host_config.strategies import CodexHostStrategy
|
|
16
|
+
from hatch.mcp_host_config.models import MCPServerConfig, HostConfiguration
|
|
17
|
+
from hatch.mcp_host_config.backup import MCPHostConfigBackupManager, BackupInfo
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestCodexBackupIntegration(unittest.TestCase):
|
|
21
|
+
"""Test suite for Codex backup integration."""
|
|
22
|
+
|
|
23
|
+
def setUp(self):
|
|
24
|
+
"""Set up test environment."""
|
|
25
|
+
self.strategy = CodexHostStrategy()
|
|
26
|
+
|
|
27
|
+
@regression_test
|
|
28
|
+
def test_write_configuration_creates_backup_by_default(self):
|
|
29
|
+
"""Test that write_configuration creates backup by default when file exists."""
|
|
30
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
31
|
+
config_path = Path(tmpdir) / "config.toml"
|
|
32
|
+
backup_dir = Path(tmpdir) / "backups"
|
|
33
|
+
|
|
34
|
+
# Create initial config
|
|
35
|
+
initial_toml = """[mcp_servers.old-server]
|
|
36
|
+
command = "old-command"
|
|
37
|
+
"""
|
|
38
|
+
config_path.write_text(initial_toml)
|
|
39
|
+
|
|
40
|
+
# Create new configuration
|
|
41
|
+
new_config = HostConfiguration(servers={
|
|
42
|
+
'new-server': MCPServerConfig(
|
|
43
|
+
command='new-command',
|
|
44
|
+
args=['--test']
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
# Patch paths
|
|
49
|
+
from unittest.mock import patch
|
|
50
|
+
with patch.object(self.strategy, 'get_config_path', return_value=config_path):
|
|
51
|
+
with patch('hatch.mcp_host_config.strategies.MCPHostConfigBackupManager') as MockBackupManager:
|
|
52
|
+
# Create a real backup manager with custom backup dir
|
|
53
|
+
real_backup_manager = MCPHostConfigBackupManager(backup_root=backup_dir)
|
|
54
|
+
MockBackupManager.return_value = real_backup_manager
|
|
55
|
+
|
|
56
|
+
# Write configuration (should create backup)
|
|
57
|
+
success = self.strategy.write_configuration(new_config, no_backup=False)
|
|
58
|
+
self.assertTrue(success)
|
|
59
|
+
|
|
60
|
+
# Verify backup was created
|
|
61
|
+
backup_files = list(backup_dir.glob('codex/*.toml.*'))
|
|
62
|
+
self.assertGreater(len(backup_files), 0, "Backup file should be created")
|
|
63
|
+
|
|
64
|
+
@regression_test
|
|
65
|
+
def test_write_configuration_skips_backup_when_requested(self):
|
|
66
|
+
"""Test that write_configuration skips backup when no_backup=True."""
|
|
67
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
68
|
+
config_path = Path(tmpdir) / "config.toml"
|
|
69
|
+
backup_dir = Path(tmpdir) / "backups"
|
|
70
|
+
|
|
71
|
+
# Create initial config
|
|
72
|
+
initial_toml = """[mcp_servers.old-server]
|
|
73
|
+
command = "old-command"
|
|
74
|
+
"""
|
|
75
|
+
config_path.write_text(initial_toml)
|
|
76
|
+
|
|
77
|
+
# Create new configuration
|
|
78
|
+
new_config = HostConfiguration(servers={
|
|
79
|
+
'new-server': MCPServerConfig(
|
|
80
|
+
command='new-command'
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
# Patch paths
|
|
85
|
+
from unittest.mock import patch
|
|
86
|
+
with patch.object(self.strategy, 'get_config_path', return_value=config_path):
|
|
87
|
+
with patch('hatch.mcp_host_config.strategies.MCPHostConfigBackupManager') as MockBackupManager:
|
|
88
|
+
real_backup_manager = MCPHostConfigBackupManager(backup_root=backup_dir)
|
|
89
|
+
MockBackupManager.return_value = real_backup_manager
|
|
90
|
+
|
|
91
|
+
# Write configuration with no_backup=True
|
|
92
|
+
success = self.strategy.write_configuration(new_config, no_backup=True)
|
|
93
|
+
self.assertTrue(success)
|
|
94
|
+
|
|
95
|
+
# Verify no backup was created
|
|
96
|
+
if backup_dir.exists():
|
|
97
|
+
backup_files = list(backup_dir.glob('codex/*.toml.*'))
|
|
98
|
+
self.assertEqual(len(backup_files), 0, "No backup should be created when no_backup=True")
|
|
99
|
+
|
|
100
|
+
@regression_test
|
|
101
|
+
def test_write_configuration_no_backup_for_new_file(self):
|
|
102
|
+
"""Test that no backup is created when writing to a new file."""
|
|
103
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
104
|
+
config_path = Path(tmpdir) / "config.toml"
|
|
105
|
+
backup_dir = Path(tmpdir) / "backups"
|
|
106
|
+
|
|
107
|
+
# Don't create initial file - this is a new file
|
|
108
|
+
|
|
109
|
+
# Create new configuration
|
|
110
|
+
new_config = HostConfiguration(servers={
|
|
111
|
+
'new-server': MCPServerConfig(
|
|
112
|
+
command='new-command'
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
# Patch paths
|
|
117
|
+
from unittest.mock import patch
|
|
118
|
+
with patch.object(self.strategy, 'get_config_path', return_value=config_path):
|
|
119
|
+
with patch('hatch.mcp_host_config.strategies.MCPHostConfigBackupManager') as MockBackupManager:
|
|
120
|
+
real_backup_manager = MCPHostConfigBackupManager(backup_root=backup_dir)
|
|
121
|
+
MockBackupManager.return_value = real_backup_manager
|
|
122
|
+
|
|
123
|
+
# Write configuration to new file
|
|
124
|
+
success = self.strategy.write_configuration(new_config, no_backup=False)
|
|
125
|
+
self.assertTrue(success)
|
|
126
|
+
|
|
127
|
+
# Verify file was created
|
|
128
|
+
self.assertTrue(config_path.exists())
|
|
129
|
+
|
|
130
|
+
# Verify no backup was created (nothing to backup)
|
|
131
|
+
if backup_dir.exists():
|
|
132
|
+
backup_files = list(backup_dir.glob('codex/*.toml.*'))
|
|
133
|
+
self.assertEqual(len(backup_files), 0, "No backup for new file")
|
|
134
|
+
|
|
135
|
+
@regression_test
|
|
136
|
+
def test_codex_hostname_supported_in_backup_system(self):
|
|
137
|
+
"""Test that 'codex' hostname is supported by the backup system."""
|
|
138
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
139
|
+
config_path = Path(tmpdir) / "config.toml"
|
|
140
|
+
backup_dir = Path(tmpdir) / "backups"
|
|
141
|
+
|
|
142
|
+
# Create a config file
|
|
143
|
+
config_path.write_text("[mcp_servers.test]\ncommand = 'test'\n")
|
|
144
|
+
|
|
145
|
+
# Create backup manager
|
|
146
|
+
backup_manager = MCPHostConfigBackupManager(backup_root=backup_dir)
|
|
147
|
+
|
|
148
|
+
# Create backup with 'codex' hostname - should not raise validation error
|
|
149
|
+
result = backup_manager.create_backup(config_path, 'codex')
|
|
150
|
+
|
|
151
|
+
# Verify backup succeeded
|
|
152
|
+
self.assertTrue(result.success, "Backup with 'codex' hostname should succeed")
|
|
153
|
+
self.assertIsNotNone(result.backup_path)
|
|
154
|
+
|
|
155
|
+
# Verify backup filename follows pattern
|
|
156
|
+
backup_filename = result.backup_path.name
|
|
157
|
+
self.assertTrue(backup_filename.startswith('config.toml.codex.'))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == '__main__':
|
|
161
|
+
unittest.main()
|
|
162
|
+
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codex MCP Host Strategy Tests
|
|
3
|
+
|
|
4
|
+
Tests for CodexHostStrategy implementation including path resolution,
|
|
5
|
+
configuration read/write, TOML handling, and host detection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
import tempfile
|
|
10
|
+
import tomllib
|
|
11
|
+
from unittest.mock import patch, mock_open, MagicMock
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from wobble.decorators import regression_test
|
|
15
|
+
|
|
16
|
+
from hatch.mcp_host_config.strategies import CodexHostStrategy
|
|
17
|
+
from hatch.mcp_host_config.models import MCPServerConfig, HostConfiguration
|
|
18
|
+
|
|
19
|
+
# Import test data loader from local tests module
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
23
|
+
from test_data_utils import MCPHostConfigTestDataLoader
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestCodexHostStrategy(unittest.TestCase):
|
|
27
|
+
"""Test suite for CodexHostStrategy implementation."""
|
|
28
|
+
|
|
29
|
+
def setUp(self):
|
|
30
|
+
"""Set up test environment."""
|
|
31
|
+
self.strategy = CodexHostStrategy()
|
|
32
|
+
self.test_data_loader = MCPHostConfigTestDataLoader()
|
|
33
|
+
|
|
34
|
+
@regression_test
|
|
35
|
+
def test_codex_config_path_resolution(self):
|
|
36
|
+
"""Test Codex configuration path resolution."""
|
|
37
|
+
config_path = self.strategy.get_config_path()
|
|
38
|
+
|
|
39
|
+
# Verify path structure (use normalized path for cross-platform compatibility)
|
|
40
|
+
self.assertIsNotNone(config_path)
|
|
41
|
+
normalized_path = str(config_path).replace('\\', '/')
|
|
42
|
+
self.assertTrue(normalized_path.endswith('.codex/config.toml'))
|
|
43
|
+
self.assertEqual(config_path.name, 'config.toml')
|
|
44
|
+
self.assertEqual(config_path.suffix, '.toml') # Verify TOML extension
|
|
45
|
+
|
|
46
|
+
@regression_test
|
|
47
|
+
def test_codex_config_key(self):
|
|
48
|
+
"""Test Codex configuration key."""
|
|
49
|
+
config_key = self.strategy.get_config_key()
|
|
50
|
+
# Codex uses underscore, not camelCase
|
|
51
|
+
self.assertEqual(config_key, "mcp_servers")
|
|
52
|
+
self.assertNotEqual(config_key, "mcpServers") # Verify different from other hosts
|
|
53
|
+
|
|
54
|
+
@regression_test
|
|
55
|
+
def test_codex_server_config_validation_stdio(self):
|
|
56
|
+
"""Test Codex STDIO server configuration validation."""
|
|
57
|
+
# Test local server validation
|
|
58
|
+
local_config = MCPServerConfig(
|
|
59
|
+
command="npx",
|
|
60
|
+
args=["-y", "package"]
|
|
61
|
+
)
|
|
62
|
+
self.assertTrue(self.strategy.validate_server_config(local_config))
|
|
63
|
+
|
|
64
|
+
@regression_test
|
|
65
|
+
def test_codex_server_config_validation_http(self):
|
|
66
|
+
"""Test Codex HTTP server configuration validation."""
|
|
67
|
+
# Test remote server validation
|
|
68
|
+
remote_config = MCPServerConfig(
|
|
69
|
+
url="https://api.example.com/mcp"
|
|
70
|
+
)
|
|
71
|
+
self.assertTrue(self.strategy.validate_server_config(remote_config))
|
|
72
|
+
|
|
73
|
+
@patch('pathlib.Path.exists')
|
|
74
|
+
@regression_test
|
|
75
|
+
def test_codex_host_availability_detection(self, mock_exists):
|
|
76
|
+
"""Test Codex host availability detection."""
|
|
77
|
+
# Test when Codex directory exists
|
|
78
|
+
mock_exists.return_value = True
|
|
79
|
+
self.assertTrue(self.strategy.is_host_available())
|
|
80
|
+
|
|
81
|
+
# Test when Codex directory doesn't exist
|
|
82
|
+
mock_exists.return_value = False
|
|
83
|
+
self.assertFalse(self.strategy.is_host_available())
|
|
84
|
+
|
|
85
|
+
@regression_test
|
|
86
|
+
def test_codex_read_configuration_success(self):
|
|
87
|
+
"""Test successful Codex TOML configuration reading."""
|
|
88
|
+
# Load test data
|
|
89
|
+
test_toml_path = Path(__file__).parent.parent / "test_data" / "codex" / "valid_config.toml"
|
|
90
|
+
|
|
91
|
+
with patch.object(self.strategy, 'get_config_path', return_value=test_toml_path):
|
|
92
|
+
config = self.strategy.read_configuration()
|
|
93
|
+
|
|
94
|
+
# Verify configuration was read
|
|
95
|
+
self.assertIsInstance(config, HostConfiguration)
|
|
96
|
+
self.assertIn('context7', config.servers)
|
|
97
|
+
|
|
98
|
+
# Verify server details
|
|
99
|
+
server = config.servers['context7']
|
|
100
|
+
self.assertEqual(server.command, 'npx')
|
|
101
|
+
self.assertEqual(server.args, ['-y', '@upstash/context7-mcp'])
|
|
102
|
+
|
|
103
|
+
# Verify nested env section was parsed correctly
|
|
104
|
+
self.assertIsNotNone(server.env)
|
|
105
|
+
self.assertEqual(server.env.get('MY_VAR'), 'value')
|
|
106
|
+
|
|
107
|
+
@regression_test
|
|
108
|
+
def test_codex_read_configuration_file_not_exists(self):
|
|
109
|
+
"""Test Codex configuration reading when file doesn't exist."""
|
|
110
|
+
non_existent_path = Path("/non/existent/path/config.toml")
|
|
111
|
+
|
|
112
|
+
with patch.object(self.strategy, 'get_config_path', return_value=non_existent_path):
|
|
113
|
+
config = self.strategy.read_configuration()
|
|
114
|
+
|
|
115
|
+
# Should return empty configuration without error
|
|
116
|
+
self.assertIsInstance(config, HostConfiguration)
|
|
117
|
+
self.assertEqual(len(config.servers), 0)
|
|
118
|
+
|
|
119
|
+
@regression_test
|
|
120
|
+
def test_codex_write_configuration_preserves_features(self):
|
|
121
|
+
"""Test that write_configuration preserves [features] section."""
|
|
122
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
123
|
+
config_path = Path(tmpdir) / "config.toml"
|
|
124
|
+
|
|
125
|
+
# Create initial config with features section
|
|
126
|
+
initial_toml = """[features]
|
|
127
|
+
rmcp_client = true
|
|
128
|
+
|
|
129
|
+
[mcp_servers.existing]
|
|
130
|
+
command = "old-command"
|
|
131
|
+
"""
|
|
132
|
+
config_path.write_text(initial_toml)
|
|
133
|
+
|
|
134
|
+
# Create new configuration to write
|
|
135
|
+
new_config = HostConfiguration(servers={
|
|
136
|
+
'new-server': MCPServerConfig(
|
|
137
|
+
command='new-command',
|
|
138
|
+
args=['--test']
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
# Write configuration
|
|
143
|
+
with patch.object(self.strategy, 'get_config_path', return_value=config_path):
|
|
144
|
+
success = self.strategy.write_configuration(new_config, no_backup=True)
|
|
145
|
+
self.assertTrue(success)
|
|
146
|
+
|
|
147
|
+
# Read back and verify features section preserved
|
|
148
|
+
with open(config_path, 'rb') as f:
|
|
149
|
+
result_data = tomllib.load(f)
|
|
150
|
+
|
|
151
|
+
# Verify features section preserved
|
|
152
|
+
self.assertIn('features', result_data)
|
|
153
|
+
self.assertTrue(result_data['features'].get('rmcp_client'))
|
|
154
|
+
|
|
155
|
+
# Verify new server added
|
|
156
|
+
self.assertIn('mcp_servers', result_data)
|
|
157
|
+
self.assertIn('new-server', result_data['mcp_servers'])
|
|
158
|
+
self.assertEqual(result_data['mcp_servers']['new-server']['command'], 'new-command')
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == '__main__':
|
|
162
|
+
unittest.main()
|
|
163
|
+
|