mcp-proxy-adapter 4.1.1__py3-none-any.whl → 6.0.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 (200) hide show
  1. mcp_proxy_adapter/__main__.py +32 -0
  2. mcp_proxy_adapter/api/app.py +290 -33
  3. mcp_proxy_adapter/api/handlers.py +32 -6
  4. mcp_proxy_adapter/api/middleware/__init__.py +38 -32
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +243 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/protocol_middleware.py +201 -0
  10. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  11. mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
  12. mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
  13. mcp_proxy_adapter/commands/__init__.py +19 -4
  14. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  15. mcp_proxy_adapter/commands/base.py +66 -32
  16. mcp_proxy_adapter/commands/builtin_commands.py +95 -0
  17. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  18. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  19. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  20. mcp_proxy_adapter/commands/command_registry.py +711 -354
  21. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  22. mcp_proxy_adapter/commands/echo_command.py +81 -0
  23. mcp_proxy_adapter/commands/health_command.py +8 -1
  24. mcp_proxy_adapter/commands/help_command.py +21 -14
  25. mcp_proxy_adapter/commands/hooks.py +200 -167
  26. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  27. mcp_proxy_adapter/commands/load_command.py +176 -0
  28. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  29. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  30. mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
  31. mcp_proxy_adapter/commands/reload_command.py +48 -50
  32. mcp_proxy_adapter/commands/result.py +1 -0
  33. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  34. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  35. mcp_proxy_adapter/commands/security_command.py +488 -0
  36. mcp_proxy_adapter/commands/ssl_setup_command.py +366 -0
  37. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  38. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  39. mcp_proxy_adapter/commands/unload_command.py +158 -0
  40. mcp_proxy_adapter/config.py +394 -14
  41. mcp_proxy_adapter/core/app_factory.py +410 -0
  42. mcp_proxy_adapter/core/app_runner.py +272 -0
  43. mcp_proxy_adapter/core/auth_validator.py +606 -0
  44. mcp_proxy_adapter/core/certificate_utils.py +1045 -0
  45. mcp_proxy_adapter/core/client.py +574 -0
  46. mcp_proxy_adapter/core/client_manager.py +284 -0
  47. mcp_proxy_adapter/core/client_security.py +384 -0
  48. mcp_proxy_adapter/core/config_converter.py +405 -0
  49. mcp_proxy_adapter/core/config_validator.py +218 -0
  50. mcp_proxy_adapter/core/logging.py +19 -3
  51. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  52. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  53. mcp_proxy_adapter/core/protocol_manager.py +385 -0
  54. mcp_proxy_adapter/core/proxy_client.py +602 -0
  55. mcp_proxy_adapter/core/proxy_registration.py +522 -0
  56. mcp_proxy_adapter/core/role_utils.py +426 -0
  57. mcp_proxy_adapter/core/security_adapter.py +370 -0
  58. mcp_proxy_adapter/core/security_factory.py +239 -0
  59. mcp_proxy_adapter/core/security_integration.py +286 -0
  60. mcp_proxy_adapter/core/server_adapter.py +282 -0
  61. mcp_proxy_adapter/core/server_engine.py +270 -0
  62. mcp_proxy_adapter/core/settings.py +1 -0
  63. mcp_proxy_adapter/core/ssl_utils.py +234 -0
  64. mcp_proxy_adapter/core/transport_manager.py +292 -0
  65. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  66. mcp_proxy_adapter/custom_openapi.py +22 -11
  67. mcp_proxy_adapter/examples/__init__.py +13 -4
  68. mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
  69. mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
  70. mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
  71. mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
  72. mcp_proxy_adapter/examples/commands/__init__.py +5 -0
  73. mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
  74. mcp_proxy_adapter/examples/debug_request_state.py +112 -0
  75. mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
  76. mcp_proxy_adapter/examples/demo_client.py +275 -0
  77. mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
  78. mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
  79. mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
  80. mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
  81. mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
  82. mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
  83. mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
  84. mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  85. mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
  86. mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
  87. mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  88. mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
  89. mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
  90. mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
  91. mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
  92. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
  93. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  94. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
  95. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
  96. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  97. mcp_proxy_adapter/examples/full_application/main.py +173 -0
  98. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
  99. mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
  100. mcp_proxy_adapter/examples/generate_certificates.py +177 -0
  101. mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
  102. mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
  103. mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
  104. mcp_proxy_adapter/examples/run_example.py +59 -0
  105. mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
  106. mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
  107. mcp_proxy_adapter/examples/run_security_tests.py +544 -0
  108. mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
  109. mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
  110. mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
  111. mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
  112. mcp_proxy_adapter/examples/security_test_client.py +782 -0
  113. mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
  114. mcp_proxy_adapter/examples/test_config.py +148 -0
  115. mcp_proxy_adapter/examples/test_config_generator.py +86 -0
  116. mcp_proxy_adapter/examples/test_examples.py +281 -0
  117. mcp_proxy_adapter/examples/universal_client.py +620 -0
  118. mcp_proxy_adapter/main.py +93 -0
  119. mcp_proxy_adapter/utils/config_generator.py +1008 -0
  120. mcp_proxy_adapter/version.py +5 -2
  121. mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
  122. mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
  123. mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
  124. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
  125. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  126. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  127. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  128. mcp_proxy_adapter/examples/README.md +0 -124
  129. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  130. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  131. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  132. mcp_proxy_adapter/examples/basic_server/config.json +0 -35
  133. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  134. mcp_proxy_adapter/examples/basic_server/server.py +0 -103
  135. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  136. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  137. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
  138. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  139. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  140. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  141. mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
  142. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  143. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  144. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  145. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  146. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  147. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  148. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  149. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  150. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  151. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  152. mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
  153. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  154. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  155. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  156. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  157. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  158. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  159. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  160. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  161. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  162. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  163. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  164. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  165. mcp_proxy_adapter/tests/__init__.py +0 -0
  166. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  167. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  168. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  169. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  170. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  171. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  172. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  173. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  174. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  175. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  176. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  177. mcp_proxy_adapter/tests/conftest.py +0 -131
  178. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  179. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  180. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  181. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  182. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  183. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  184. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  185. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  186. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  187. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  188. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  189. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  190. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  191. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  192. mcp_proxy_adapter/tests/test_config.py +0 -127
  193. mcp_proxy_adapter/tests/test_utils.py +0 -65
  194. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  195. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  196. mcp_proxy_adapter/tests/unit/test_config.py +0 -217
  197. mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
  198. mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
  199. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
  200. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,544 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Security Test Runner for MCP Proxy Adapter
4
+ This script runs comprehensive security tests against all server configurations:
5
+ - Basic HTTP
6
+ - HTTP + Token authentication
7
+ - HTTPS
8
+ - HTTPS + Token authentication
9
+ - mTLS
10
+ Author: Vasiliy Zdanovskiy
11
+ email: vasilyvz@gmail.com
12
+ """
13
+ import asyncio
14
+ import json
15
+ import os
16
+ import signal
17
+ import socket
18
+ import subprocess
19
+ import sys
20
+ import time
21
+ from pathlib import Path
22
+ from typing import Dict, List, Optional, Any, Tuple
23
+
24
+ import psutil
25
+ import requests
26
+ # Import security test client with proper module path
27
+ from mcp_proxy_adapter.examples.security_test_client import SecurityTestClient, TestResult
28
+ class SecurityTestRunner:
29
+ """Main test runner for security testing."""
30
+ def __init__(self):
31
+ """Initialize test runner."""
32
+ self.servers = {}
33
+ self.proxy_server = None
34
+ self.server_logs = {}
35
+ self.proxy_log = None
36
+ self.test_results = {}
37
+ # Base and proxy ports
38
+ self.base_port = 20000
39
+ self.proxy_port = 20010
40
+ # Server configurations with ports starting from 20000
41
+ self.configs = {
42
+ "basic_http": {
43
+ "config": "configs/http_simple.json",
44
+ "port": self.base_port + 0,
45
+ "url": f"http://localhost:{self.base_port + 0}",
46
+ "auth": "none"
47
+ },
48
+ "http_token": {
49
+ "config": "configs/http_token.json",
50
+ "port": self.base_port + 1,
51
+ "url": f"http://localhost:{self.base_port + 1}",
52
+ "auth": "api_key"
53
+ },
54
+ "https": {
55
+ "config": "configs/https_simple.json",
56
+ "port": self.base_port + 2,
57
+ "url": f"https://localhost:{self.base_port + 2}",
58
+ "auth": "none"
59
+ },
60
+ "https_token": {
61
+ "config": "configs/https_token.json",
62
+ "port": self.base_port + 3,
63
+ "url": f"https://localhost:{self.base_port + 3}",
64
+ "auth": "api_key"
65
+ },
66
+ "mtls": {
67
+ "config": "configs/mtls_no_roles.json",
68
+ "port": self.base_port + 4,
69
+ "url": f"https://localhost:{self.base_port + 4}",
70
+ "auth": "certificate"
71
+ }
72
+ }
73
+
74
+ def _port_in_use(self, port: int, host: str = "127.0.0.1") -> bool:
75
+ try:
76
+ with socket.create_connection((host, port), timeout=0.5):
77
+ return True
78
+ except Exception:
79
+ return False
80
+
81
+ def _pids_on_port(self, port: int) -> List[int]:
82
+ pids: List[int] = []
83
+ try:
84
+ for proc in psutil.process_iter(attrs=["pid"]):
85
+ try:
86
+ connections = proc.connections(kind="inet")
87
+ for c in connections:
88
+ if c.laddr and c.laddr.port == port:
89
+ pids.append(proc.pid)
90
+ break
91
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
92
+ pass
93
+ except Exception:
94
+ pass
95
+ return list(set(pids))
96
+
97
+ def ensure_ports_free(self, ports: List[int]) -> None:
98
+ for port in ports:
99
+ pids = self._pids_on_port(port)
100
+ for pid in pids:
101
+ try:
102
+ psutil.Process(pid).terminate()
103
+ except Exception:
104
+ pass
105
+ time.sleep(0.3)
106
+ for pid in pids:
107
+ try:
108
+ if psutil.pid_exists(pid):
109
+ psutil.Process(pid).kill()
110
+ except Exception:
111
+ pass
112
+
113
+ def wait_for_http(self, url: str, timeout_sec: float = 8.0) -> bool:
114
+ end = time.time() + timeout_sec
115
+ candidates = ["/health", "/proxy/health"]
116
+ while time.time() < end:
117
+ for path in candidates:
118
+ health_url = url.rstrip("/") + path
119
+ try:
120
+ resp = requests.get(health_url, timeout=1.0, verify=False)
121
+ if resp.status_code == 200:
122
+ return True
123
+ except Exception:
124
+ pass
125
+ time.sleep(0.2)
126
+ return False
127
+
128
+ def wait_for_port(self, port: int, timeout_sec: float = 8.0) -> bool:
129
+ end = time.time() + timeout_sec
130
+ while time.time() < end:
131
+ if self._port_in_use(port):
132
+ return True
133
+ time.sleep(0.2)
134
+ return False
135
+
136
+ def get_all_ports(self) -> List[int]:
137
+ ports = [self.proxy_port]
138
+ for cfg in self.configs.values():
139
+ ports.append(cfg["port"])
140
+ return list(sorted(set(ports)))
141
+
142
+ def _validate_file(self, base: Path, path_value: Optional[str]) -> Tuple[bool, str]:
143
+ if not path_value:
144
+ return True, ""
145
+ p = Path(path_value)
146
+ if not p.is_absolute():
147
+ p = base / p
148
+ return p.exists(), str(p)
149
+
150
+ def validate_config_files(self) -> bool:
151
+ ok = True
152
+ base = Path.cwd()
153
+ missing: List[str] = []
154
+ for name, cfg in self.configs.items():
155
+ cfg_path = Path(cfg["config"]).resolve()
156
+ try:
157
+ with open(cfg_path, "r", encoding="utf-8") as f:
158
+ data = json.load(f)
159
+ ssl = data.get("ssl", {})
160
+ for key in ("cert_file", "key_file", "ca_cert"):
161
+ exists, abs_path = self._validate_file(base, ssl.get(key))
162
+ if ssl.get("enabled") and key in ("cert_file", "key_file") and not exists:
163
+ ok = False
164
+ missing.append(f"{name}:{key} -> {abs_path}")
165
+ sec = data.get("security", {})
166
+ perms = sec.get("permissions", {})
167
+ exists, abs_path = self._validate_file(base, perms.get("roles_file"))
168
+ if sec.get("enabled") and perms.get("enabled") and not exists:
169
+ ok = False
170
+ missing.append(f"{name}:roles_file -> {abs_path}")
171
+ except Exception as e:
172
+ ok = False
173
+ missing.append(f"{name}: cannot read {cfg_path} ({e})")
174
+ if not ok:
175
+ print("❌ CONFIG VALIDATION FAILED. Missing files:")
176
+ for m in missing:
177
+ print(" -", m)
178
+ else:
179
+ print("✅ Configuration file paths validated")
180
+ return ok
181
+ def check_prerequisites(self) -> bool:
182
+ """Check if all prerequisites are met."""
183
+ print("🔍 Checking prerequisites...")
184
+ # Check if we're in the right directory
185
+ if not Path("configs").exists():
186
+ print("❌ configs directory not found. Please run from the test environment root directory.")
187
+ return False
188
+ # Check if certificates exist
189
+ cert_files = [
190
+ "certs/mcp_proxy_adapter_ca_ca.crt",
191
+ "certs/localhost_server.crt",
192
+ "keys/localhost_server.key"
193
+ ]
194
+
195
+ missing_certs = []
196
+ # Check if roles.json exists
197
+ roles_file = "configs/roles.json"
198
+ if not os.path.exists(roles_file):
199
+ missing_certs.append(f"Missing roles file: {roles_file}")
200
+ for cert_file in cert_files:
201
+ if not Path(cert_file).exists():
202
+ missing_certs.append(cert_file)
203
+ if missing_certs:
204
+ print(f"❌ Missing certificates: {missing_certs}")
205
+ print("💡 Run: python -m mcp_proxy_adapter.examples.setup_test_environment to generate certificates")
206
+ return False
207
+ print("✅ Prerequisites check passed")
208
+ return True
209
+ def start_server(self, name: str, config_path: str, port: int) -> Optional[subprocess.Popen]:
210
+ """Start a server in background."""
211
+ try:
212
+ print(f"🚀 Starting {name} server on port {port}...")
213
+ # Ensure the port is free just before starting
214
+ self.ensure_ports_free([port])
215
+ if self._port_in_use(port):
216
+ print(f"⚠️ Port {port} still busy, waiting...")
217
+ if not self.wait_for_port(port, timeout_sec=1.5):
218
+ # After wait_for_port True means busy; we invert logic here
219
+ pass
220
+ # If still busy after attempts, abort this server start
221
+ if self._port_in_use(port):
222
+ print(f"❌ Port {port} is in use, cannot start {name}")
223
+ return None
224
+ # Start server in background
225
+ logs_dir = Path("logs"); logs_dir.mkdir(exist_ok=True)
226
+ log_path = logs_dir / f"{name}.log"
227
+ log_file = open(log_path, "wb")
228
+ self.server_logs[name] = log_file
229
+ process = subprocess.Popen([
230
+ sys.executable, "-m", "mcp_proxy_adapter.main",
231
+ "--config", config_path
232
+ ], stdout=log_file, stderr=subprocess.STDOUT)
233
+ # Wait a bit for server to start
234
+ time.sleep(3)
235
+ # Check if process is still running
236
+ if process.poll() is None:
237
+ print(f"✅ {name} server started (PID: {process.pid})")
238
+ return process
239
+ else:
240
+ print(f"❌ Failed to start {name} server (see logs/{name}.log)")
241
+ return None
242
+ except Exception as e:
243
+ print(f"❌ Error starting {name} server: {e}")
244
+ return None
245
+ def stop_server(self, name: str, process: subprocess.Popen):
246
+ """Stop a server."""
247
+ try:
248
+ print(f"🛑 Stopping {name} server (PID: {process.pid})...")
249
+ process.terminate()
250
+ # Wait for graceful shutdown
251
+ try:
252
+ process.wait(timeout=5)
253
+ print(f"✅ {name} server stopped")
254
+ except subprocess.TimeoutExpired:
255
+ print(f"⚠️ Force killing {name} server")
256
+ process.kill()
257
+ process.wait()
258
+ except Exception as e:
259
+ print(f"❌ Error stopping {name} server: {e}")
260
+ finally:
261
+ try:
262
+ lf = self.server_logs.pop(name, None)
263
+ if lf:
264
+ lf.close()
265
+ except Exception:
266
+ pass
267
+
268
+ def start_proxy_server(self) -> bool:
269
+ """Start the proxy server for server registration."""
270
+ try:
271
+ print("🚀 Starting proxy server...")
272
+ # Find the proxy server script
273
+ proxy_script = Path(__file__).parent / "run_proxy_server.py"
274
+ if not proxy_script.exists():
275
+ print("❌ Proxy server script not found")
276
+ return False
277
+
278
+ # Start proxy server
279
+ cmd = [sys.executable, str(proxy_script), "--host", "127.0.0.1", "--port", str(self.proxy_port)]
280
+ logs_dir = Path("logs"); logs_dir.mkdir(exist_ok=True)
281
+ proxy_log_path = logs_dir / "proxy_server.log"
282
+ self.proxy_log = open(proxy_log_path, "wb")
283
+ process = subprocess.Popen(
284
+ cmd,
285
+ stdout=self.proxy_log,
286
+ stderr=subprocess.STDOUT,
287
+ cwd=Path.cwd()
288
+ )
289
+
290
+ # Check readiness
291
+ if process.poll() is None and self.wait_for_http(f"http://127.0.0.1:{self.proxy_port}"):
292
+ self.proxy_server = process
293
+ print("✅ Proxy server started successfully (PID: {})".format(process.pid))
294
+ return True
295
+ else:
296
+ print("❌ Failed to start proxy server (see logs/proxy_server.log)")
297
+ return False
298
+
299
+ except Exception as e:
300
+ print(f"❌ Error starting proxy server: {e}")
301
+ return False
302
+
303
+ def stop_proxy_server(self):
304
+ """Stop the proxy server."""
305
+ if self.proxy_server:
306
+ try:
307
+ print("🛑 Stopping proxy server (PID: {})...".format(self.proxy_server.pid))
308
+ self.proxy_server.terminate()
309
+ try:
310
+ self.proxy_server.wait(timeout=5)
311
+ print("✅ Proxy server stopped")
312
+ except subprocess.TimeoutExpired:
313
+ print("⚠️ Force killing proxy server")
314
+ self.proxy_server.kill()
315
+ self.proxy_server.wait()
316
+ except Exception as e:
317
+ print(f"❌ Error stopping proxy server: {e}")
318
+ finally:
319
+ self.proxy_server = None
320
+ try:
321
+ if self.proxy_log:
322
+ self.proxy_log.close()
323
+ self.proxy_log = None
324
+ except Exception:
325
+ pass
326
+ async def test_server(self, name: str, config: Dict[str, Any]) -> List[TestResult]:
327
+ """Test a specific server configuration."""
328
+ print(f"\n🧪 Testing {name} server...")
329
+ print("=" * 50)
330
+ # Create client with appropriate SSL context
331
+ if config["auth"] == "certificate":
332
+ # For mTLS, create client with certificate-based SSL context
333
+ client = SecurityTestClient(config["url"])
334
+ # Override SSL context for mTLS
335
+ client.create_ssl_context = client.create_ssl_context_for_mtls
336
+ async with client as client_session:
337
+ results = await client_session.run_security_tests(
338
+ config["url"],
339
+ config["auth"]
340
+ )
341
+ else:
342
+ # For other auth types, use default SSL context
343
+ async with SecurityTestClient(config["url"]) as client:
344
+ results = await client.run_security_tests(
345
+ config["url"],
346
+ config["auth"]
347
+ )
348
+ # Print summary for this server
349
+ passed = sum(1 for r in results if r.success)
350
+ total = len(results)
351
+ print(f"\n📊 {name} Results: {passed}/{total} tests passed")
352
+ return results
353
+ async def run_all_tests(self) -> Dict[str, List[TestResult]]:
354
+ """Run tests against all server configurations."""
355
+ print("🚀 Starting comprehensive security testing")
356
+ print("=" * 60)
357
+ # Start all servers with verification and abort on failure
358
+ for name, config in self.configs.items():
359
+ process = self.start_server(name, config["config"], config["port"])
360
+ if not process:
361
+ print(f"❌ {name} failed to start. Aborting.")
362
+ return {}
363
+ url = config["url"]
364
+ ready = False
365
+ if name == "mtls":
366
+ ready = self.wait_for_port(config["port"], timeout_sec=8.0)
367
+ else:
368
+ ready = self.wait_for_http(url, timeout_sec=8.0)
369
+ if not ready:
370
+ print(f"❌ {name} did not become ready. Aborting.")
371
+ return {}
372
+ self.servers[name] = process
373
+ print("\n✅ All servers started and verified. Proceeding to client tests...")
374
+ # Test each server
375
+ all_results = {}
376
+ for name, config in self.configs.items():
377
+ if name in self.servers:
378
+ try:
379
+ results = await self.test_server(name, config)
380
+ all_results[name] = results
381
+ except Exception as e:
382
+ print(f"❌ Error testing {name}: {e}")
383
+ all_results[name] = []
384
+ else:
385
+ print(f"⚠️ Skipping {name} tests (server not running)")
386
+ all_results[name] = []
387
+ return all_results
388
+ def print_final_summary(self, all_results: Dict[str, List[TestResult]]):
389
+ """Print final test summary."""
390
+ print("\n" + "=" * 80)
391
+ print("📊 FINAL SECURITY TEST SUMMARY")
392
+ print("=" * 80)
393
+ total_tests = 0
394
+ total_passed = 0
395
+ for server_name, results in all_results.items():
396
+ if results:
397
+ passed = sum(1 for r in results if r.success)
398
+ total = len(results)
399
+ total_tests += total
400
+ total_passed += passed
401
+ status = "✅ PASS" if passed == total else "❌ FAIL"
402
+ print(f"{status} {server_name.upper()}: {passed}/{total} tests passed")
403
+ # Show failed tests
404
+ failed_tests = [r for r in results if not r.success]
405
+ for test in failed_tests:
406
+ print(f" ❌ {test.test_name}: {test.error_message}")
407
+ else:
408
+ print(f"⚠️ SKIP {server_name.upper()}: No tests run")
409
+ print("\n" + "-" * 80)
410
+ print(f"OVERALL: {total_passed}/{total_tests} tests passed")
411
+ if total_tests > 0:
412
+ success_rate = (total_passed / total_tests) * 100
413
+ print(f"SUCCESS RATE: {success_rate:.1f}%")
414
+ # Overall status
415
+ if total_passed == total_tests and total_tests > 0:
416
+ print("🎉 ALL TESTS PASSED!")
417
+ print("\n" + "=" * 60)
418
+ print("✅ SECURITY TESTS COMPLETED SUCCESSFULLY")
419
+ print("=" * 60)
420
+ print("\n📋 NEXT STEPS:")
421
+ print("1. Start basic framework example:")
422
+ print(" python -m mcp_proxy_adapter.examples.basic_framework.main --config configs/https_simple.json")
423
+ print("\n2. Start full application example:")
424
+ print(" python -m mcp_proxy_adapter.examples.full_application.main --config configs/mtls_with_roles.json")
425
+ print("\n3. Test with custom configurations:")
426
+ print(" python -m mcp_proxy_adapter.examples.basic_framework.main --config configs/http_simple.json")
427
+ print("=" * 60)
428
+ elif total_passed > 0:
429
+ print("⚠️ SOME TESTS FAILED")
430
+ print("\n🔧 TROUBLESHOOTING:")
431
+ print("1. Check if proxy server is running:")
432
+ print(" python /path/to/run_proxy_server.py --host 127.0.0.1 --port 3004")
433
+ print("\n2. Check if certificates are generated:")
434
+ print(" python -m mcp_proxy_adapter.examples.generate_certificates")
435
+ print("\n3. Verify configuration files exist:")
436
+ print(" python -m mcp_proxy_adapter.examples.generate_test_configs --output-dir configs")
437
+ print("\n4. Check if ports are available (3004, 8000-8005)")
438
+ print("=" * 60)
439
+ else:
440
+ print("❌ ALL TESTS FAILED")
441
+ print("\n🔧 TROUBLESHOOTING:")
442
+ print("1. Run setup test environment:")
443
+ print(" python -m mcp_proxy_adapter.examples.setup_test_environment")
444
+ print("\n2. Generate certificates:")
445
+ print(" python -m mcp_proxy_adapter.examples.generate_certificates")
446
+ print("\n3. Generate configurations:")
447
+ print(" python -m mcp_proxy_adapter.examples.generate_test_configs --output-dir configs")
448
+ print("\n4. Start proxy server manually if needed:")
449
+ print(" python /path/to/run_proxy_server.py --host 127.0.0.1 --port 3004")
450
+ print("=" * 60)
451
+ def cleanup(self):
452
+ """Cleanup all running servers and proxy."""
453
+ print("\n🧹 Cleaning up...")
454
+ # Stop test servers
455
+ for name, process in self.servers.items():
456
+ self.stop_server(name, process)
457
+ self.servers.clear()
458
+ # Stop proxy server
459
+ self.stop_proxy_server()
460
+ def signal_handler(self, signum, frame):
461
+ """Handle interrupt signals."""
462
+ print(f"\n⚠️ Received signal {signum}, cleaning up...")
463
+ self.cleanup()
464
+ sys.exit(0)
465
+ async def run(self):
466
+ """Main run method."""
467
+ # Set up signal handlers
468
+ signal.signal(signal.SIGINT, self.signal_handler)
469
+ signal.signal(signal.SIGTERM, self.signal_handler)
470
+ try:
471
+ # Check prerequisites
472
+ if not self.check_prerequisites():
473
+ return False
474
+
475
+ # Free ports before run
476
+ print("\n🧹 Freeing ports before startup...")
477
+ self.ensure_ports_free(self.get_all_ports())
478
+
479
+ # Validate config file paths
480
+ if not self.validate_config_files():
481
+ return False
482
+
483
+ # Start proxy server first
484
+ print("\n🚀 Starting proxy server for server registration...")
485
+ if not self.start_proxy_server():
486
+ print("❌ Cannot proceed without proxy server")
487
+ return False
488
+
489
+ # Wait for proxy server to be fully ready
490
+ print("⏳ Waiting for proxy server to be ready...")
491
+ time.sleep(3)
492
+
493
+ # Run all tests
494
+ all_results = await self.run_all_tests()
495
+ # Print summary
496
+ self.print_final_summary(all_results)
497
+ return True
498
+ except Exception as e:
499
+ print(f"❌ Test runner error: {e}")
500
+ return False
501
+ finally:
502
+ # Always cleanup
503
+ self.cleanup()
504
+ def main():
505
+ """Main function."""
506
+ import argparse
507
+ parser = argparse.ArgumentParser(description="Security Test Runner for MCP Proxy Adapter")
508
+ parser.add_argument("--config", help="Test specific configuration")
509
+ parser.add_argument("--no-cleanup", action="store_true", help="Don't cleanup servers after tests")
510
+ parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
511
+ args = parser.parse_args()
512
+
513
+ # Determine the correct configs directory
514
+ current_dir = Path.cwd()
515
+ if (current_dir / "configs").exists():
516
+ # We're in the test environment root directory
517
+ configs_dir = current_dir / "configs"
518
+ os.chdir(current_dir) # Stay in current directory
519
+ elif (Path(__file__).parent.parent / "configs").exists():
520
+ # We're running from package installation, configs is relative to examples
521
+ configs_dir = Path(__file__).parent.parent / "configs"
522
+ os.chdir(Path(__file__).parent.parent) # Change to parent of examples
523
+ else:
524
+ # Try to find configs relative to examples directory
525
+ examples_dir = Path(__file__).parent
526
+ configs_dir = examples_dir / "configs"
527
+ os.chdir(examples_dir)
528
+
529
+ print(f"🔍 Using configs directory: {configs_dir}")
530
+ print(f"🔍 Working directory: {Path.cwd()}")
531
+
532
+ # Create and run test runner
533
+ runner = SecurityTestRunner()
534
+ try:
535
+ success = asyncio.run(runner.run())
536
+ sys.exit(0 if success else 1)
537
+ except KeyboardInterrupt:
538
+ print("\n⚠️ Interrupted by user")
539
+ sys.exit(1)
540
+ except Exception as e:
541
+ print(f"❌ Unexpected error: {e}")
542
+ sys.exit(1)
543
+ if __name__ == "__main__":
544
+ main()