hatch-xclam 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hatch/__init__.py +21 -0
- hatch/cli_hatch.py +2748 -0
- hatch/environment_manager.py +1375 -0
- hatch/installers/__init__.py +25 -0
- hatch/installers/dependency_installation_orchestrator.py +636 -0
- hatch/installers/docker_installer.py +545 -0
- hatch/installers/hatch_installer.py +198 -0
- hatch/installers/installation_context.py +109 -0
- hatch/installers/installer_base.py +195 -0
- hatch/installers/python_installer.py +342 -0
- hatch/installers/registry.py +179 -0
- hatch/installers/system_installer.py +588 -0
- hatch/mcp_host_config/__init__.py +38 -0
- hatch/mcp_host_config/backup.py +458 -0
- hatch/mcp_host_config/host_management.py +572 -0
- hatch/mcp_host_config/models.py +602 -0
- hatch/mcp_host_config/reporting.py +181 -0
- hatch/mcp_host_config/strategies.py +513 -0
- hatch/package_loader.py +263 -0
- hatch/python_environment_manager.py +734 -0
- hatch/registry_explorer.py +171 -0
- hatch/registry_retriever.py +335 -0
- hatch/template_generator.py +179 -0
- hatch_xclam-0.7.0.dist-info/METADATA +150 -0
- hatch_xclam-0.7.0.dist-info/RECORD +93 -0
- hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
- hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
- hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
- hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/run_environment_tests.py +124 -0
- tests/test_cli_version.py +122 -0
- tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
- tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
- tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
- tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
- tests/test_data_utils.py +472 -0
- tests/test_dependency_orchestrator_consent.py +266 -0
- tests/test_docker_installer.py +524 -0
- tests/test_env_manip.py +991 -0
- tests/test_hatch_installer.py +179 -0
- tests/test_installer_base.py +221 -0
- tests/test_mcp_atomic_operations.py +276 -0
- tests/test_mcp_backup_integration.py +308 -0
- tests/test_mcp_cli_all_host_specific_args.py +303 -0
- tests/test_mcp_cli_backup_management.py +295 -0
- tests/test_mcp_cli_direct_management.py +453 -0
- tests/test_mcp_cli_discovery_listing.py +582 -0
- tests/test_mcp_cli_host_config_integration.py +823 -0
- tests/test_mcp_cli_package_management.py +360 -0
- tests/test_mcp_cli_partial_updates.py +859 -0
- tests/test_mcp_environment_integration.py +520 -0
- tests/test_mcp_host_config_backup.py +257 -0
- tests/test_mcp_host_configuration_manager.py +331 -0
- tests/test_mcp_host_registry_decorator.py +348 -0
- tests/test_mcp_pydantic_architecture_v4.py +603 -0
- tests/test_mcp_server_config_models.py +242 -0
- tests/test_mcp_server_config_type_field.py +221 -0
- tests/test_mcp_sync_functionality.py +316 -0
- tests/test_mcp_user_feedback_reporting.py +359 -0
- tests/test_non_tty_integration.py +281 -0
- tests/test_online_package_loader.py +202 -0
- tests/test_python_environment_manager.py +882 -0
- tests/test_python_installer.py +327 -0
- tests/test_registry.py +51 -0
- tests/test_registry_retriever.py +250 -0
- tests/test_system_installer.py +733 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test suite for consolidated MCPServerConfig Pydantic model.
|
|
3
|
+
|
|
4
|
+
This module tests the consolidated MCPServerConfig model that supports
|
|
5
|
+
both local and remote server configurations with proper validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Add the parent directory to the path to import wobble
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from wobble.decorators import regression_test, integration_test
|
|
17
|
+
except ImportError:
|
|
18
|
+
# Fallback decorators if wobble is not available
|
|
19
|
+
def regression_test(func):
|
|
20
|
+
return func
|
|
21
|
+
|
|
22
|
+
def integration_test(scope="component"):
|
|
23
|
+
def decorator(func):
|
|
24
|
+
return func
|
|
25
|
+
return decorator
|
|
26
|
+
|
|
27
|
+
from test_data_utils import MCPHostConfigTestDataLoader
|
|
28
|
+
from hatch.mcp_host_config.models import MCPServerConfig
|
|
29
|
+
from pydantic import ValidationError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestMCPServerConfigModels(unittest.TestCase):
|
|
33
|
+
"""Test suite for consolidated MCPServerConfig Pydantic model."""
|
|
34
|
+
|
|
35
|
+
def setUp(self):
|
|
36
|
+
"""Set up test environment."""
|
|
37
|
+
self.test_data_loader = MCPHostConfigTestDataLoader()
|
|
38
|
+
|
|
39
|
+
@regression_test
|
|
40
|
+
def test_mcp_server_config_local_server_validation_success(self):
|
|
41
|
+
"""Test successful local server configuration validation."""
|
|
42
|
+
config_data = self.test_data_loader.load_mcp_server_config("local")
|
|
43
|
+
config = MCPServerConfig(**config_data)
|
|
44
|
+
|
|
45
|
+
self.assertEqual(config.command, "python")
|
|
46
|
+
self.assertEqual(len(config.args), 3)
|
|
47
|
+
self.assertEqual(config.env["API_KEY"], "test")
|
|
48
|
+
self.assertTrue(config.is_local_server)
|
|
49
|
+
self.assertFalse(config.is_remote_server)
|
|
50
|
+
|
|
51
|
+
@regression_test
|
|
52
|
+
def test_mcp_server_config_remote_server_validation_success(self):
|
|
53
|
+
"""Test successful remote server configuration validation."""
|
|
54
|
+
config_data = self.test_data_loader.load_mcp_server_config("remote")
|
|
55
|
+
config = MCPServerConfig(**config_data)
|
|
56
|
+
|
|
57
|
+
self.assertEqual(config.url, "https://api.example.com/mcp")
|
|
58
|
+
self.assertEqual(config.headers["Authorization"], "Bearer token")
|
|
59
|
+
self.assertFalse(config.is_local_server)
|
|
60
|
+
self.assertTrue(config.is_remote_server)
|
|
61
|
+
|
|
62
|
+
@regression_test
|
|
63
|
+
def test_mcp_server_config_validation_fails_both_command_and_url(self):
|
|
64
|
+
"""Test validation fails when both command and URL are provided."""
|
|
65
|
+
config_data = {
|
|
66
|
+
"command": "python",
|
|
67
|
+
"args": ["server.py"],
|
|
68
|
+
"url": "https://example.com/mcp" # Invalid: both command and URL
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
with self.assertRaises(ValidationError) as context:
|
|
72
|
+
MCPServerConfig(**config_data)
|
|
73
|
+
|
|
74
|
+
self.assertIn("Cannot specify both 'command' and 'url'", str(context.exception))
|
|
75
|
+
|
|
76
|
+
@regression_test
|
|
77
|
+
def test_mcp_server_config_validation_fails_neither_command_nor_url(self):
|
|
78
|
+
"""Test validation fails when neither command nor URL are provided."""
|
|
79
|
+
config_data = {
|
|
80
|
+
"env": {"TEST": "value"}
|
|
81
|
+
# Missing both command and url
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
with self.assertRaises(ValidationError) as context:
|
|
85
|
+
MCPServerConfig(**config_data)
|
|
86
|
+
|
|
87
|
+
self.assertIn("Either 'command' (local server) or 'url' (remote server) must be provided",
|
|
88
|
+
str(context.exception))
|
|
89
|
+
|
|
90
|
+
@regression_test
|
|
91
|
+
def test_mcp_server_config_validation_args_without_command_fails(self):
|
|
92
|
+
"""Test validation fails when args provided without command."""
|
|
93
|
+
config_data = {
|
|
94
|
+
"url": "https://example.com/mcp",
|
|
95
|
+
"args": ["--flag"] # Invalid: args without command
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
with self.assertRaises(ValidationError) as context:
|
|
99
|
+
MCPServerConfig(**config_data)
|
|
100
|
+
|
|
101
|
+
self.assertIn("'args' can only be specified with 'command'", str(context.exception))
|
|
102
|
+
|
|
103
|
+
@regression_test
|
|
104
|
+
def test_mcp_server_config_validation_headers_without_url_fails(self):
|
|
105
|
+
"""Test validation fails when headers provided without URL."""
|
|
106
|
+
config_data = {
|
|
107
|
+
"command": "python",
|
|
108
|
+
"headers": {"Authorization": "Bearer token"} # Invalid: headers without URL
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
with self.assertRaises(ValidationError) as context:
|
|
112
|
+
MCPServerConfig(**config_data)
|
|
113
|
+
|
|
114
|
+
self.assertIn("'headers' can only be specified with 'url'", str(context.exception))
|
|
115
|
+
|
|
116
|
+
@regression_test
|
|
117
|
+
def test_mcp_server_config_url_format_validation(self):
|
|
118
|
+
"""Test URL format validation."""
|
|
119
|
+
invalid_urls = ["ftp://example.com", "example.com", "not-a-url"]
|
|
120
|
+
|
|
121
|
+
for invalid_url in invalid_urls:
|
|
122
|
+
with self.assertRaises(ValidationError):
|
|
123
|
+
MCPServerConfig(url=invalid_url)
|
|
124
|
+
|
|
125
|
+
@regression_test
|
|
126
|
+
def test_mcp_server_config_no_future_extension_fields(self):
|
|
127
|
+
"""Test that extra fields are allowed for host-specific extensions."""
|
|
128
|
+
# Current design allows extra fields to support host-specific configurations
|
|
129
|
+
# (e.g., Gemini's timeout, VS Code's envFile, etc.)
|
|
130
|
+
config_data = {
|
|
131
|
+
"command": "python",
|
|
132
|
+
"timeout": 30, # Allowed (host-specific field)
|
|
133
|
+
"retry_attempts": 3, # Allowed (host-specific field)
|
|
134
|
+
"ssl_verify": True # Allowed (host-specific field)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Should NOT raise ValidationError (extra="allow")
|
|
138
|
+
config = MCPServerConfig(**config_data)
|
|
139
|
+
|
|
140
|
+
# Verify core fields are set correctly
|
|
141
|
+
self.assertEqual(config.command, "python")
|
|
142
|
+
|
|
143
|
+
# Note: In Phase 3B, strict validation will be enforced in host-specific models
|
|
144
|
+
|
|
145
|
+
@regression_test
|
|
146
|
+
def test_mcp_server_config_command_empty_validation(self):
|
|
147
|
+
"""Test validation fails for empty command."""
|
|
148
|
+
config_data = {
|
|
149
|
+
"command": " ", # Empty/whitespace command
|
|
150
|
+
"args": ["server.py"]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
with self.assertRaises(ValidationError) as context:
|
|
154
|
+
MCPServerConfig(**config_data)
|
|
155
|
+
|
|
156
|
+
self.assertIn("Command cannot be empty", str(context.exception))
|
|
157
|
+
|
|
158
|
+
@regression_test
|
|
159
|
+
def test_mcp_server_config_command_strip_whitespace(self):
|
|
160
|
+
"""Test command whitespace is stripped."""
|
|
161
|
+
config_data = {
|
|
162
|
+
"command": " python ",
|
|
163
|
+
"args": ["server.py"]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
config = MCPServerConfig(**config_data)
|
|
167
|
+
self.assertEqual(config.command, "python")
|
|
168
|
+
|
|
169
|
+
@regression_test
|
|
170
|
+
def test_mcp_server_config_minimal_local_server(self):
|
|
171
|
+
"""Test minimal local server configuration."""
|
|
172
|
+
config_data = self.test_data_loader.load_mcp_server_config("local_minimal")
|
|
173
|
+
config = MCPServerConfig(**config_data)
|
|
174
|
+
|
|
175
|
+
self.assertEqual(config.command, "python")
|
|
176
|
+
self.assertEqual(config.args, ["minimal_server.py"])
|
|
177
|
+
self.assertIsNone(config.env)
|
|
178
|
+
self.assertTrue(config.is_local_server)
|
|
179
|
+
self.assertFalse(config.is_remote_server)
|
|
180
|
+
|
|
181
|
+
@regression_test
|
|
182
|
+
def test_mcp_server_config_minimal_remote_server(self):
|
|
183
|
+
"""Test minimal remote server configuration."""
|
|
184
|
+
config_data = self.test_data_loader.load_mcp_server_config("remote_minimal")
|
|
185
|
+
config = MCPServerConfig(**config_data)
|
|
186
|
+
|
|
187
|
+
self.assertEqual(config.url, "https://minimal.example.com/mcp")
|
|
188
|
+
self.assertIsNone(config.headers)
|
|
189
|
+
self.assertFalse(config.is_local_server)
|
|
190
|
+
self.assertTrue(config.is_remote_server)
|
|
191
|
+
|
|
192
|
+
@regression_test
|
|
193
|
+
def test_mcp_server_config_serialization_roundtrip(self):
|
|
194
|
+
"""Test serialization and deserialization roundtrip."""
|
|
195
|
+
# Test local server
|
|
196
|
+
local_config_data = self.test_data_loader.load_mcp_server_config("local")
|
|
197
|
+
local_config = MCPServerConfig(**local_config_data)
|
|
198
|
+
|
|
199
|
+
# Serialize and deserialize
|
|
200
|
+
serialized = local_config.model_dump()
|
|
201
|
+
roundtrip_config = MCPServerConfig(**serialized)
|
|
202
|
+
|
|
203
|
+
self.assertEqual(local_config.command, roundtrip_config.command)
|
|
204
|
+
self.assertEqual(local_config.args, roundtrip_config.args)
|
|
205
|
+
self.assertEqual(local_config.env, roundtrip_config.env)
|
|
206
|
+
self.assertEqual(local_config.is_local_server, roundtrip_config.is_local_server)
|
|
207
|
+
|
|
208
|
+
# Test remote server
|
|
209
|
+
remote_config_data = self.test_data_loader.load_mcp_server_config("remote")
|
|
210
|
+
remote_config = MCPServerConfig(**remote_config_data)
|
|
211
|
+
|
|
212
|
+
# Serialize and deserialize
|
|
213
|
+
serialized = remote_config.model_dump()
|
|
214
|
+
roundtrip_config = MCPServerConfig(**serialized)
|
|
215
|
+
|
|
216
|
+
self.assertEqual(remote_config.url, roundtrip_config.url)
|
|
217
|
+
self.assertEqual(remote_config.headers, roundtrip_config.headers)
|
|
218
|
+
self.assertEqual(remote_config.is_remote_server, roundtrip_config.is_remote_server)
|
|
219
|
+
|
|
220
|
+
@regression_test
|
|
221
|
+
def test_mcp_server_config_json_serialization(self):
|
|
222
|
+
"""Test JSON serialization compatibility."""
|
|
223
|
+
import json
|
|
224
|
+
|
|
225
|
+
config_data = self.test_data_loader.load_mcp_server_config("local")
|
|
226
|
+
config = MCPServerConfig(**config_data)
|
|
227
|
+
|
|
228
|
+
# Test JSON serialization
|
|
229
|
+
json_str = config.model_dump_json()
|
|
230
|
+
self.assertIsInstance(json_str, str)
|
|
231
|
+
|
|
232
|
+
# Test JSON deserialization
|
|
233
|
+
parsed_data = json.loads(json_str)
|
|
234
|
+
roundtrip_config = MCPServerConfig(**parsed_data)
|
|
235
|
+
|
|
236
|
+
self.assertEqual(config.command, roundtrip_config.command)
|
|
237
|
+
self.assertEqual(config.args, roundtrip_config.args)
|
|
238
|
+
self.assertEqual(config.env, roundtrip_config.env)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == '__main__':
|
|
242
|
+
unittest.main()
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test suite for MCPServerConfig type field (Phase 3A).
|
|
3
|
+
|
|
4
|
+
This module tests the type field addition to MCPServerConfig model,
|
|
5
|
+
including validation and property behavior.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Add the parent directory to the path to import wobble
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from wobble.decorators import regression_test
|
|
17
|
+
except ImportError:
|
|
18
|
+
# Fallback decorator if wobble is not available
|
|
19
|
+
def regression_test(func):
|
|
20
|
+
return func
|
|
21
|
+
|
|
22
|
+
from hatch.mcp_host_config.models import MCPServerConfig
|
|
23
|
+
from pydantic import ValidationError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestMCPServerConfigTypeField(unittest.TestCase):
|
|
27
|
+
"""Test suite for MCPServerConfig type field validation."""
|
|
28
|
+
|
|
29
|
+
@regression_test
|
|
30
|
+
def test_type_stdio_with_command_success(self):
|
|
31
|
+
"""Test successful stdio type with command."""
|
|
32
|
+
config = MCPServerConfig(
|
|
33
|
+
name="test-server",
|
|
34
|
+
type="stdio",
|
|
35
|
+
command="python",
|
|
36
|
+
args=["server.py"]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
self.assertEqual(config.type, "stdio")
|
|
40
|
+
self.assertEqual(config.command, "python")
|
|
41
|
+
self.assertTrue(config.is_local_server)
|
|
42
|
+
self.assertFalse(config.is_remote_server)
|
|
43
|
+
|
|
44
|
+
@regression_test
|
|
45
|
+
def test_type_sse_with_url_success(self):
|
|
46
|
+
"""Test successful sse type with url."""
|
|
47
|
+
config = MCPServerConfig(
|
|
48
|
+
name="test-server",
|
|
49
|
+
type="sse",
|
|
50
|
+
url="https://api.example.com/mcp"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self.assertEqual(config.type, "sse")
|
|
54
|
+
self.assertEqual(config.url, "https://api.example.com/mcp")
|
|
55
|
+
self.assertFalse(config.is_local_server)
|
|
56
|
+
self.assertTrue(config.is_remote_server)
|
|
57
|
+
|
|
58
|
+
@regression_test
|
|
59
|
+
def test_type_http_with_url_success(self):
|
|
60
|
+
"""Test successful http type with url."""
|
|
61
|
+
config = MCPServerConfig(
|
|
62
|
+
name="test-server",
|
|
63
|
+
type="http",
|
|
64
|
+
url="https://api.example.com/mcp",
|
|
65
|
+
headers={"Authorization": "Bearer token"}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.assertEqual(config.type, "http")
|
|
69
|
+
self.assertEqual(config.url, "https://api.example.com/mcp")
|
|
70
|
+
self.assertFalse(config.is_local_server)
|
|
71
|
+
self.assertTrue(config.is_remote_server)
|
|
72
|
+
|
|
73
|
+
@regression_test
|
|
74
|
+
def test_type_stdio_without_command_fails(self):
|
|
75
|
+
"""Test validation fails when type=stdio without command."""
|
|
76
|
+
with self.assertRaises(ValidationError) as context:
|
|
77
|
+
MCPServerConfig(
|
|
78
|
+
name="test-server",
|
|
79
|
+
type="stdio",
|
|
80
|
+
url="https://api.example.com/mcp" # Invalid: stdio with url
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self.assertIn("'type=stdio' requires 'command' field", str(context.exception))
|
|
84
|
+
|
|
85
|
+
@regression_test
|
|
86
|
+
def test_type_stdio_with_url_fails(self):
|
|
87
|
+
"""Test validation fails when type=stdio with url."""
|
|
88
|
+
with self.assertRaises(ValidationError) as context:
|
|
89
|
+
MCPServerConfig(
|
|
90
|
+
name="test-server",
|
|
91
|
+
type="stdio",
|
|
92
|
+
command="python",
|
|
93
|
+
url="https://api.example.com/mcp" # Invalid: both command and url
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# The validate_server_type() validator catches this first
|
|
97
|
+
self.assertIn("Cannot specify both 'command' and 'url'", str(context.exception))
|
|
98
|
+
|
|
99
|
+
@regression_test
|
|
100
|
+
def test_type_sse_without_url_fails(self):
|
|
101
|
+
"""Test validation fails when type=sse without url."""
|
|
102
|
+
with self.assertRaises(ValidationError) as context:
|
|
103
|
+
MCPServerConfig(
|
|
104
|
+
name="test-server",
|
|
105
|
+
type="sse",
|
|
106
|
+
command="python" # Invalid: sse with command
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.assertIn("'type=sse' requires 'url' field", str(context.exception))
|
|
110
|
+
|
|
111
|
+
@regression_test
|
|
112
|
+
def test_type_http_without_url_fails(self):
|
|
113
|
+
"""Test validation fails when type=http without url."""
|
|
114
|
+
with self.assertRaises(ValidationError) as context:
|
|
115
|
+
MCPServerConfig(
|
|
116
|
+
name="test-server",
|
|
117
|
+
type="http",
|
|
118
|
+
command="python" # Invalid: http with command
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self.assertIn("'type=http' requires 'url' field", str(context.exception))
|
|
122
|
+
|
|
123
|
+
@regression_test
|
|
124
|
+
def test_type_sse_with_command_fails(self):
|
|
125
|
+
"""Test validation fails when type=sse with command."""
|
|
126
|
+
with self.assertRaises(ValidationError) as context:
|
|
127
|
+
MCPServerConfig(
|
|
128
|
+
name="test-server",
|
|
129
|
+
type="sse",
|
|
130
|
+
command="python",
|
|
131
|
+
url="https://api.example.com/mcp" # Invalid: both command and url
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# The validate_server_type() validator catches this first
|
|
135
|
+
self.assertIn("Cannot specify both 'command' and 'url'", str(context.exception))
|
|
136
|
+
|
|
137
|
+
@regression_test
|
|
138
|
+
def test_backward_compatibility_no_type_field_local(self):
|
|
139
|
+
"""Test backward compatibility: local server without type field."""
|
|
140
|
+
config = MCPServerConfig(
|
|
141
|
+
name="test-server",
|
|
142
|
+
command="python",
|
|
143
|
+
args=["server.py"]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.assertIsNone(config.type)
|
|
147
|
+
self.assertEqual(config.command, "python")
|
|
148
|
+
self.assertTrue(config.is_local_server)
|
|
149
|
+
self.assertFalse(config.is_remote_server)
|
|
150
|
+
|
|
151
|
+
@regression_test
|
|
152
|
+
def test_backward_compatibility_no_type_field_remote(self):
|
|
153
|
+
"""Test backward compatibility: remote server without type field."""
|
|
154
|
+
config = MCPServerConfig(
|
|
155
|
+
name="test-server",
|
|
156
|
+
url="https://api.example.com/mcp"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
self.assertIsNone(config.type)
|
|
160
|
+
self.assertEqual(config.url, "https://api.example.com/mcp")
|
|
161
|
+
self.assertFalse(config.is_local_server)
|
|
162
|
+
self.assertTrue(config.is_remote_server)
|
|
163
|
+
|
|
164
|
+
@regression_test
|
|
165
|
+
def test_type_field_with_env_variables(self):
|
|
166
|
+
"""Test type field with environment variables."""
|
|
167
|
+
config = MCPServerConfig(
|
|
168
|
+
name="test-server",
|
|
169
|
+
type="stdio",
|
|
170
|
+
command="python",
|
|
171
|
+
args=["server.py"],
|
|
172
|
+
env={"API_KEY": "test-key", "DEBUG": "true"}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self.assertEqual(config.type, "stdio")
|
|
176
|
+
self.assertEqual(config.env["API_KEY"], "test-key")
|
|
177
|
+
self.assertEqual(config.env["DEBUG"], "true")
|
|
178
|
+
|
|
179
|
+
@regression_test
|
|
180
|
+
def test_type_field_serialization(self):
|
|
181
|
+
"""Test type field is included in serialization."""
|
|
182
|
+
config = MCPServerConfig(
|
|
183
|
+
name="test-server",
|
|
184
|
+
type="stdio",
|
|
185
|
+
command="python",
|
|
186
|
+
args=["server.py"]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Test model_dump includes type field
|
|
190
|
+
data = config.model_dump()
|
|
191
|
+
self.assertEqual(data["type"], "stdio")
|
|
192
|
+
self.assertEqual(data["command"], "python")
|
|
193
|
+
|
|
194
|
+
# Test JSON serialization
|
|
195
|
+
import json
|
|
196
|
+
json_str = config.model_dump_json()
|
|
197
|
+
parsed = json.loads(json_str)
|
|
198
|
+
self.assertEqual(parsed["type"], "stdio")
|
|
199
|
+
|
|
200
|
+
@regression_test
|
|
201
|
+
def test_type_field_roundtrip(self):
|
|
202
|
+
"""Test type field survives serialization roundtrip."""
|
|
203
|
+
original = MCPServerConfig(
|
|
204
|
+
name="test-server",
|
|
205
|
+
type="sse",
|
|
206
|
+
url="https://api.example.com/mcp",
|
|
207
|
+
headers={"Authorization": "Bearer token"}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Serialize and deserialize
|
|
211
|
+
data = original.model_dump()
|
|
212
|
+
roundtrip = MCPServerConfig(**data)
|
|
213
|
+
|
|
214
|
+
self.assertEqual(roundtrip.type, "sse")
|
|
215
|
+
self.assertEqual(roundtrip.url, "https://api.example.com/mcp")
|
|
216
|
+
self.assertEqual(roundtrip.headers["Authorization"], "Bearer token")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == '__main__':
|
|
220
|
+
unittest.main()
|
|
221
|
+
|