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.
- mcp_proxy_adapter/__main__.py +32 -0
- mcp_proxy_adapter/api/app.py +290 -33
- mcp_proxy_adapter/api/handlers.py +32 -6
- mcp_proxy_adapter/api/middleware/__init__.py +38 -32
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
- mcp_proxy_adapter/api/middleware/factory.py +243 -0
- mcp_proxy_adapter/api/middleware/logging.py +32 -6
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +201 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
- mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
- mcp_proxy_adapter/commands/__init__.py +19 -4
- mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
- mcp_proxy_adapter/commands/base.py +66 -32
- mcp_proxy_adapter/commands/builtin_commands.py +95 -0
- mcp_proxy_adapter/commands/catalog_manager.py +838 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
- mcp_proxy_adapter/commands/command_registry.py +711 -354
- mcp_proxy_adapter/commands/dependency_manager.py +245 -0
- mcp_proxy_adapter/commands/echo_command.py +81 -0
- mcp_proxy_adapter/commands/health_command.py +8 -1
- mcp_proxy_adapter/commands/help_command.py +21 -14
- mcp_proxy_adapter/commands/hooks.py +200 -167
- mcp_proxy_adapter/commands/key_management_command.py +506 -0
- mcp_proxy_adapter/commands/load_command.py +176 -0
- mcp_proxy_adapter/commands/plugins_command.py +235 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
- mcp_proxy_adapter/commands/reload_command.py +48 -50
- mcp_proxy_adapter/commands/result.py +1 -0
- mcp_proxy_adapter/commands/role_test_command.py +141 -0
- mcp_proxy_adapter/commands/roles_management_command.py +697 -0
- mcp_proxy_adapter/commands/security_command.py +488 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +366 -0
- mcp_proxy_adapter/commands/token_management_command.py +529 -0
- mcp_proxy_adapter/commands/transport_management_command.py +144 -0
- mcp_proxy_adapter/commands/unload_command.py +158 -0
- mcp_proxy_adapter/config.py +394 -14
- mcp_proxy_adapter/core/app_factory.py +410 -0
- mcp_proxy_adapter/core/app_runner.py +272 -0
- mcp_proxy_adapter/core/auth_validator.py +606 -0
- mcp_proxy_adapter/core/certificate_utils.py +1045 -0
- mcp_proxy_adapter/core/client.py +574 -0
- mcp_proxy_adapter/core/client_manager.py +284 -0
- mcp_proxy_adapter/core/client_security.py +384 -0
- mcp_proxy_adapter/core/config_converter.py +405 -0
- mcp_proxy_adapter/core/config_validator.py +218 -0
- mcp_proxy_adapter/core/logging.py +19 -3
- mcp_proxy_adapter/core/mtls_asgi.py +156 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/protocol_manager.py +385 -0
- mcp_proxy_adapter/core/proxy_client.py +602 -0
- mcp_proxy_adapter/core/proxy_registration.py +522 -0
- mcp_proxy_adapter/core/role_utils.py +426 -0
- mcp_proxy_adapter/core/security_adapter.py +370 -0
- mcp_proxy_adapter/core/security_factory.py +239 -0
- mcp_proxy_adapter/core/security_integration.py +286 -0
- mcp_proxy_adapter/core/server_adapter.py +282 -0
- mcp_proxy_adapter/core/server_engine.py +270 -0
- mcp_proxy_adapter/core/settings.py +1 -0
- mcp_proxy_adapter/core/ssl_utils.py +234 -0
- mcp_proxy_adapter/core/transport_manager.py +292 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/custom_openapi.py +22 -11
- mcp_proxy_adapter/examples/__init__.py +13 -4
- mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/commands/__init__.py +5 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
- mcp_proxy_adapter/examples/debug_request_state.py +112 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
- mcp_proxy_adapter/examples/demo_client.py +275 -0
- mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
- mcp_proxy_adapter/examples/generate_certificates.py +177 -0
- mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
- mcp_proxy_adapter/examples/run_example.py +59 -0
- mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
- mcp_proxy_adapter/examples/run_security_tests.py +544 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
- mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
- mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
- mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/security_test_client.py +782 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
- mcp_proxy_adapter/examples/test_config.py +148 -0
- mcp_proxy_adapter/examples/test_config_generator.py +86 -0
- mcp_proxy_adapter/examples/test_examples.py +281 -0
- mcp_proxy_adapter/examples/universal_client.py +620 -0
- mcp_proxy_adapter/main.py +93 -0
- mcp_proxy_adapter/utils/config_generator.py +1008 -0
- mcp_proxy_adapter/version.py +5 -2
- mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
- mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
- mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
- mcp_proxy_adapter/api/middleware/auth.py +0 -146
- mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
- mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
- mcp_proxy_adapter/examples/README.md +0 -124
- mcp_proxy_adapter/examples/basic_server/README.md +0 -60
- mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
- mcp_proxy_adapter/examples/basic_server/config.json +0 -35
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
- mcp_proxy_adapter/examples/basic_server/server.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
- mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
- mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
- mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
- mcp_proxy_adapter/examples/deployment/README.md +0 -49
- mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
- mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
- mcp_proxy_adapter/examples/deployment/config.json +0 -29
- mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
- mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
- mcp_proxy_adapter/examples/deployment/run.sh +0 -43
- mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
- mcp_proxy_adapter/schemas/base_schema.json +0 -114
- mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +0 -3
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
- mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
- mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
- mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
- mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
- mcp_proxy_adapter/tests/commands/__init__.py +0 -3
- mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
- mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
- mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
- mcp_proxy_adapter/tests/conftest.py +0 -131
- mcp_proxy_adapter/tests/functional/__init__.py +0 -3
- mcp_proxy_adapter/tests/functional/test_api.py +0 -253
- mcp_proxy_adapter/tests/integration/__init__.py +0 -3
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
- mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
- mcp_proxy_adapter/tests/performance/__init__.py +0 -3
- mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
- mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
- mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
- mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
- mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
- mcp_proxy_adapter/tests/test_base_command.py +0 -123
- mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
- mcp_proxy_adapter/tests/test_command_registry.py +0 -281
- mcp_proxy_adapter/tests/test_config.py +0 -127
- mcp_proxy_adapter/tests/test_utils.py +0 -65
- mcp_proxy_adapter/tests/unit/__init__.py +0 -3
- mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
- mcp_proxy_adapter/tests/unit/test_config.py +0 -217
- mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
- mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
- {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()
|