mcp-proxy-adapter 6.2.18__py3-none-any.whl → 6.2.22__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.
@@ -14,11 +14,15 @@ import asyncio
14
14
  import json
15
15
  import os
16
16
  import signal
17
+ import socket
17
18
  import subprocess
18
19
  import sys
19
20
  import time
20
21
  from pathlib import Path
21
- from typing import Dict, List, Optional, Any
22
+ from typing import Dict, List, Optional, Any, Tuple
23
+
24
+ import psutil
25
+ import requests
22
26
  # Import security test client with proper module path
23
27
  from mcp_proxy_adapter.examples.security_test_client import SecurityTestClient, TestResult
24
28
  class SecurityTestRunner:
@@ -27,39 +31,149 @@ class SecurityTestRunner:
27
31
  """Initialize test runner."""
28
32
  self.servers = {}
29
33
  self.proxy_server = None
34
+ self.server_logs = {}
35
+ self.proxy_log = None
30
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
31
41
  self.configs = {
32
42
  "basic_http": {
33
43
  "config": "configs/http_simple.json",
34
- "port": 8000,
35
- "url": "http://localhost:8000",
44
+ "port": self.base_port + 0,
45
+ "url": f"http://localhost:{self.base_port + 0}",
36
46
  "auth": "none"
37
47
  },
38
48
  "http_token": {
39
49
  "config": "configs/http_token.json",
40
- "port": 8001,
41
- "url": "http://localhost:8001",
50
+ "port": self.base_port + 1,
51
+ "url": f"http://localhost:{self.base_port + 1}",
42
52
  "auth": "api_key"
43
53
  },
44
54
  "https": {
45
55
  "config": "configs/https_simple.json",
46
- "port": 8002,
47
- "url": "https://localhost:8002",
56
+ "port": self.base_port + 2,
57
+ "url": f"https://localhost:{self.base_port + 2}",
48
58
  "auth": "none"
49
59
  },
50
60
  "https_token": {
51
61
  "config": "configs/https_token.json",
52
- "port": 8003,
53
- "url": "https://localhost:8003",
62
+ "port": self.base_port + 3,
63
+ "url": f"https://localhost:{self.base_port + 3}",
54
64
  "auth": "api_key"
55
65
  },
56
66
  "mtls": {
57
67
  "config": "configs/mtls_no_roles.json",
58
- "port": 8004,
59
- "url": "https://localhost:8004",
68
+ "port": self.base_port + 4,
69
+ "url": f"https://localhost:{self.base_port + 4}",
60
70
  "auth": "certificate"
61
71
  }
62
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", "connections"]):
85
+ for c in proc.connections(kind="inet"):
86
+ if c.laddr and c.laddr.port == port:
87
+ pids.append(proc.pid)
88
+ break
89
+ except Exception:
90
+ pass
91
+ return list(set(pids))
92
+
93
+ def ensure_ports_free(self, ports: List[int]) -> None:
94
+ for port in ports:
95
+ pids = self._pids_on_port(port)
96
+ for pid in pids:
97
+ try:
98
+ psutil.Process(pid).terminate()
99
+ except Exception:
100
+ pass
101
+ time.sleep(0.3)
102
+ for pid in pids:
103
+ try:
104
+ if psutil.pid_exists(pid):
105
+ psutil.Process(pid).kill()
106
+ except Exception:
107
+ pass
108
+
109
+ def wait_for_http(self, url: str, timeout_sec: float = 8.0) -> bool:
110
+ end = time.time() + timeout_sec
111
+ candidates = ["/health", "/proxy/health"]
112
+ while time.time() < end:
113
+ for path in candidates:
114
+ health_url = url.rstrip("/") + path
115
+ try:
116
+ resp = requests.get(health_url, timeout=1.0, verify=False)
117
+ if resp.status_code == 200:
118
+ return True
119
+ except Exception:
120
+ pass
121
+ time.sleep(0.2)
122
+ return False
123
+
124
+ def wait_for_port(self, port: int, timeout_sec: float = 8.0) -> bool:
125
+ end = time.time() + timeout_sec
126
+ while time.time() < end:
127
+ if self._port_in_use(port):
128
+ return True
129
+ time.sleep(0.2)
130
+ return False
131
+
132
+ def get_all_ports(self) -> List[int]:
133
+ ports = [self.proxy_port]
134
+ for cfg in self.configs.values():
135
+ ports.append(cfg["port"])
136
+ return list(sorted(set(ports)))
137
+
138
+ def _validate_file(self, base: Path, path_value: Optional[str]) -> Tuple[bool, str]:
139
+ if not path_value:
140
+ return True, ""
141
+ p = Path(path_value)
142
+ if not p.is_absolute():
143
+ p = base / p
144
+ return p.exists(), str(p)
145
+
146
+ def validate_config_files(self) -> bool:
147
+ ok = True
148
+ base = Path.cwd()
149
+ missing: List[str] = []
150
+ for name, cfg in self.configs.items():
151
+ cfg_path = Path(cfg["config"]).resolve()
152
+ try:
153
+ with open(cfg_path, "r", encoding="utf-8") as f:
154
+ data = json.load(f)
155
+ ssl = data.get("ssl", {})
156
+ for key in ("cert_file", "key_file", "ca_cert"):
157
+ exists, abs_path = self._validate_file(base, ssl.get(key))
158
+ if ssl.get("enabled") and key in ("cert_file", "key_file") and not exists:
159
+ ok = False
160
+ missing.append(f"{name}:{key} -> {abs_path}")
161
+ sec = data.get("security", {})
162
+ perms = sec.get("permissions", {})
163
+ exists, abs_path = self._validate_file(base, perms.get("roles_file"))
164
+ if sec.get("enabled") and perms.get("enabled") and not exists:
165
+ ok = False
166
+ missing.append(f"{name}:roles_file -> {abs_path}")
167
+ except Exception as e:
168
+ ok = False
169
+ missing.append(f"{name}: cannot read {cfg_path} ({e})")
170
+ if not ok:
171
+ print("❌ CONFIG VALIDATION FAILED. Missing files:")
172
+ for m in missing:
173
+ print(" -", m)
174
+ else:
175
+ print("✅ Configuration file paths validated")
176
+ return ok
63
177
  def check_prerequisites(self) -> bool:
64
178
  """Check if all prerequisites are met."""
65
179
  print("🔍 Checking prerequisites...")
@@ -69,7 +183,7 @@ class SecurityTestRunner:
69
183
  return False
70
184
  # Check if certificates exist
71
185
  cert_files = [
72
- "certs/mcp_proxy_adapter_test_ca_ca.crt",
186
+ "certs/mcp_proxy_adapter_ca_ca.crt",
73
187
  "certs/localhost_server.crt",
74
188
  "keys/localhost_server.key"
75
189
  ]
@@ -87,11 +201,26 @@ class SecurityTestRunner:
87
201
  """Start a server in background."""
88
202
  try:
89
203
  print(f"🚀 Starting {name} server on port {port}...")
204
+ # Ensure the port is free just before starting
205
+ self.ensure_ports_free([port])
206
+ if self._port_in_use(port):
207
+ print(f"⚠️ Port {port} still busy, waiting...")
208
+ if not self.wait_for_port(port, timeout_sec=1.5):
209
+ # After wait_for_port True means busy; we invert logic here
210
+ pass
211
+ # If still busy after attempts, abort this server start
212
+ if self._port_in_use(port):
213
+ print(f"❌ Port {port} is in use, cannot start {name}")
214
+ return None
90
215
  # Start server in background
216
+ logs_dir = Path("logs"); logs_dir.mkdir(exist_ok=True)
217
+ log_path = logs_dir / f"{name}.log"
218
+ log_file = open(log_path, "wb")
219
+ self.server_logs[name] = log_file
91
220
  process = subprocess.Popen([
92
221
  sys.executable, "-m", "mcp_proxy_adapter.main",
93
222
  "--config", config_path
94
- ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
223
+ ], stdout=log_file, stderr=subprocess.STDOUT)
95
224
  # Wait a bit for server to start
96
225
  time.sleep(3)
97
226
  # Check if process is still running
@@ -99,10 +228,7 @@ class SecurityTestRunner:
99
228
  print(f"✅ {name} server started (PID: {process.pid})")
100
229
  return process
101
230
  else:
102
- stdout, stderr = process.communicate()
103
- print(f"❌ Failed to start {name} server:")
104
- print(f"STDOUT: {stdout.decode()}")
105
- print(f"STDERR: {stderr.decode()}")
231
+ print(f"❌ Failed to start {name} server (see logs/{name}.log)")
106
232
  return None
107
233
  except Exception as e:
108
234
  print(f"❌ Error starting {name} server: {e}")
@@ -122,6 +248,13 @@ class SecurityTestRunner:
122
248
  process.wait()
123
249
  except Exception as e:
124
250
  print(f"❌ Error stopping {name} server: {e}")
251
+ finally:
252
+ try:
253
+ lf = self.server_logs.pop(name, None)
254
+ if lf:
255
+ lf.close()
256
+ except Exception:
257
+ pass
125
258
 
126
259
  def start_proxy_server(self) -> bool:
127
260
  """Start the proxy server for server registration."""
@@ -134,27 +267,24 @@ class SecurityTestRunner:
134
267
  return False
135
268
 
136
269
  # Start proxy server
137
- cmd = [sys.executable, str(proxy_script), "--host", "127.0.0.1", "--port", "3004"]
270
+ cmd = [sys.executable, str(proxy_script), "--host", "127.0.0.1", "--port", str(self.proxy_port)]
271
+ logs_dir = Path("logs"); logs_dir.mkdir(exist_ok=True)
272
+ proxy_log_path = logs_dir / "proxy_server.log"
273
+ self.proxy_log = open(proxy_log_path, "wb")
138
274
  process = subprocess.Popen(
139
275
  cmd,
140
- stdout=subprocess.PIPE,
141
- stderr=subprocess.PIPE,
276
+ stdout=self.proxy_log,
277
+ stderr=subprocess.STDOUT,
142
278
  cwd=Path.cwd()
143
279
  )
144
280
 
145
- # Wait a bit for server to start
146
- time.sleep(2)
147
-
148
- # Check if process is still running
149
- if process.poll() is None:
281
+ # Check readiness
282
+ if process.poll() is None and self.wait_for_http(f"http://127.0.0.1:{self.proxy_port}"):
150
283
  self.proxy_server = process
151
284
  print("✅ Proxy server started successfully (PID: {})".format(process.pid))
152
285
  return True
153
286
  else:
154
- stdout, stderr = process.communicate()
155
- print("❌ Failed to start proxy server:")
156
- print("STDOUT:", stdout.decode())
157
- print("STDERR:", stderr.decode())
287
+ print("❌ Failed to start proxy server (see logs/proxy_server.log)")
158
288
  return False
159
289
 
160
290
  except Exception as e:
@@ -178,6 +308,12 @@ class SecurityTestRunner:
178
308
  print(f"❌ Error stopping proxy server: {e}")
179
309
  finally:
180
310
  self.proxy_server = None
311
+ try:
312
+ if self.proxy_log:
313
+ self.proxy_log.close()
314
+ self.proxy_log = None
315
+ except Exception:
316
+ pass
181
317
  async def test_server(self, name: str, config: Dict[str, Any]) -> List[TestResult]:
182
318
  """Test a specific server configuration."""
183
319
  print(f"\n🧪 Testing {name} server...")
@@ -209,16 +345,23 @@ class SecurityTestRunner:
209
345
  """Run tests against all server configurations."""
210
346
  print("🚀 Starting comprehensive security testing")
211
347
  print("=" * 60)
212
- # Start all servers
348
+ # Start all servers with verification and abort on failure
213
349
  for name, config in self.configs.items():
214
350
  process = self.start_server(name, config["config"], config["port"])
215
- if process:
216
- self.servers[name] = process
351
+ if not process:
352
+ print(f"❌ {name} failed to start. Aborting.")
353
+ return {}
354
+ url = config["url"]
355
+ ready = False
356
+ if name == "mtls":
357
+ ready = self.wait_for_port(config["port"], timeout_sec=8.0)
217
358
  else:
218
- print(f"⚠️ Skipping tests for {name} due to startup failure")
219
- # Wait for all servers to be ready
220
- print("\n⏳ Waiting for servers to be ready...")
221
- time.sleep(5)
359
+ ready = self.wait_for_http(url, timeout_sec=8.0)
360
+ if not ready:
361
+ print(f" {name} did not become ready. Aborting.")
362
+ return {}
363
+ self.servers[name] = process
364
+ print("\n✅ All servers started and verified. Proceeding to client tests...")
222
365
  # Test each server
223
366
  all_results = {}
224
367
  for name, config in self.configs.items():
@@ -320,6 +463,14 @@ class SecurityTestRunner:
320
463
  if not self.check_prerequisites():
321
464
  return False
322
465
 
466
+ # Free ports before run
467
+ print("\n🧹 Freeing ports before startup...")
468
+ self.ensure_ports_free(self.get_all_ports())
469
+
470
+ # Validate config file paths
471
+ if not self.validate_config_files():
472
+ return False
473
+
323
474
  # Start proxy server first
324
475
  print("\n🚀 Starting proxy server for server registration...")
325
476
  if not self.start_proxy_server():
@@ -21,6 +21,7 @@ from typing import Dict, List, Optional, Any
21
21
  from dataclasses import dataclass
22
22
  import aiohttp
23
23
  from aiohttp import ClientSession, ClientTimeout, TCPConnector
24
+
24
25
  # Add project root to path for imports
25
26
  project_root = Path(__file__).parent.parent.parent
26
27
  current_dir = Path(__file__).parent
@@ -28,6 +29,26 @@ parent_dir = current_dir.parent
28
29
  sys.path.insert(0, str(project_root))
29
30
  sys.path.insert(0, str(current_dir))
30
31
  sys.path.insert(0, str(parent_dir))
32
+
33
+ # Import mcp_security_framework components
34
+ try:
35
+ from mcp_security_framework import SSLManager, CertificateManager
36
+ from mcp_security_framework.schemas.config import SSLConfig
37
+ _MCP_SECURITY_AVAILABLE = True
38
+ print("✅ mcp_security_framework available")
39
+ except ImportError:
40
+ _MCP_SECURITY_AVAILABLE = False
41
+ print("⚠️ mcp_security_framework not available, falling back to standard SSL")
42
+
43
+ # Import cryptography components
44
+ try:
45
+ from cryptography import x509
46
+ from cryptography.hazmat.primitives import serialization
47
+ _CRYPTOGRAPHY_AVAILABLE = True
48
+ print("✅ cryptography available")
49
+ except ImportError:
50
+ _CRYPTOGRAPHY_AVAILABLE = False
51
+ print("⚠️ cryptography not available, SSL validation will be limited")
31
52
  @dataclass
32
53
  class TestResult:
33
54
  """Test result data class."""
@@ -45,11 +66,33 @@ class SecurityTestClient:
45
66
  """Initialize security test client."""
46
67
  self.base_url = base_url
47
68
  self.session: Optional[ClientSession] = None
48
- # Note: For basic testing, we'll use simple SSL context creation
49
- # instead of full mcp_security_framework integration
50
- self.security_manager = None
69
+
70
+ # Initialize security managers if available
51
71
  self.ssl_manager = None
52
- self.auth_manager = None
72
+ self.cert_manager = None
73
+ self._security_available = _MCP_SECURITY_AVAILABLE
74
+ self._crypto_available = _CRYPTOGRAPHY_AVAILABLE
75
+
76
+ if self._security_available:
77
+ try:
78
+ # Initialize SSL manager with default config
79
+ ssl_config = SSLConfig(
80
+ enabled=True,
81
+ cert_file=None,
82
+ key_file=None,
83
+ ca_cert_file=None,
84
+ verify_mode="CERT_NONE", # For testing
85
+ min_tls_version="TLSv1.2"
86
+ )
87
+ self.ssl_manager = SSLManager(ssl_config)
88
+ print("✅ SSL Manager initialized with mcp_security_framework")
89
+ except Exception as e:
90
+ print(f"⚠️ Failed to initialize SSL Manager: {e}")
91
+ self._security_available = False
92
+
93
+ if not self._security_available:
94
+ print("ℹ️ Using standard SSL library for testing")
95
+ self.ssl_manager = None
53
96
  self.test_results: List[TestResult] = []
54
97
  # Test tokens
55
98
  self.test_tokens = {
@@ -85,14 +128,33 @@ class SecurityTestClient:
85
128
  return self
86
129
  def create_ssl_context_for_mtls(self) -> ssl.SSLContext:
87
130
  """Create SSL context for mTLS connections."""
88
- ssl_context = ssl.create_default_context()
89
- # For mTLS testing
131
+ if self.ssl_manager and self._security_available:
132
+ try:
133
+ # Use mcp_security_framework for mTLS
134
+ cert_file = "./certs/user_cert.pem"
135
+ key_file = "./certs/user_key.pem"
136
+ ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
137
+
138
+ return self.ssl_manager.create_client_context(
139
+ ca_cert_file=ca_cert_file if os.path.exists(ca_cert_file) else None,
140
+ client_cert_file=cert_file if os.path.exists(cert_file) else None,
141
+ client_key_file=key_file if os.path.exists(key_file) else None,
142
+ verify_mode="CERT_NONE", # For testing
143
+ min_version="TLSv1.2"
144
+ )
145
+ except Exception as e:
146
+ print(f"⚠️ Failed to create mTLS context with mcp_security_framework: {e}")
147
+ print("ℹ️ Falling back to standard SSL")
148
+
149
+ # Fallback to standard SSL
150
+ ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
151
+ # For mTLS testing - client needs to present certificate to server
90
152
  ssl_context.check_hostname = False
91
- ssl_context.verify_mode = ssl.CERT_NONE
92
- # Load client certificate and key
153
+ ssl_context.verify_mode = ssl.CERT_NONE # Don't verify server cert for testing
154
+ # Load client certificate and key for mTLS
93
155
  cert_file = "./certs/user_cert.pem"
94
156
  key_file = "./certs/user_key.pem"
95
- ca_cert_file = "./certs/ca_cert.pem"
157
+ ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
96
158
  if os.path.exists(cert_file) and os.path.exists(key_file):
97
159
  ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
98
160
  if os.path.exists(ca_cert_file):
@@ -106,6 +168,21 @@ class SecurityTestClient:
106
168
  key_file: Optional[str] = None,
107
169
  ca_cert_file: Optional[str] = None) -> ssl.SSLContext:
108
170
  """Create SSL context for client."""
171
+ if self.ssl_manager and self._security_available:
172
+ try:
173
+ # Use mcp_security_framework for SSL context creation
174
+ return self.ssl_manager.create_client_context(
175
+ ca_cert_file=ca_cert_file if ca_cert_file and os.path.exists(ca_cert_file) else None,
176
+ client_cert_file=cert_file if cert_file and os.path.exists(cert_file) else None,
177
+ client_key_file=key_file if key_file and os.path.exists(key_file) else None,
178
+ verify_mode="CERT_NONE", # For testing
179
+ min_version="TLSv1.2"
180
+ )
181
+ except Exception as e:
182
+ print(f"⚠️ Failed to create SSL context with mcp_security_framework: {e}")
183
+ print("ℹ️ Falling back to standard SSL")
184
+
185
+ # Fallback to standard SSL
109
186
  ssl_context = ssl.create_default_context()
110
187
  # For testing with self-signed certificates
111
188
  ssl_context.check_hostname = False
@@ -121,8 +198,10 @@ class SecurityTestClient:
121
198
  """Create authentication headers."""
122
199
  headers = {"Content-Type": "application/json"}
123
200
  if auth_type == "api_key":
124
- token = kwargs.get("token", "test-token-123") # Use correct token
125
- headers["X-API-Key"] = token # Use X-API-Key header
201
+ token = kwargs.get("token", "test-token-123")
202
+ # Provide both common header styles to maximize compatibility
203
+ headers["X-API-Key"] = token
204
+ headers["Authorization"] = f"Bearer {token}"
126
205
  elif auth_type == "basic":
127
206
  username = kwargs.get("username", "admin")
128
207
  password = kwargs.get("password", "password")
@@ -306,39 +385,104 @@ class SecurityTestClient:
306
385
  start_time = time.time()
307
386
  test_name = f"Negative Auth ({auth_type})"
308
387
  try:
309
- # Use invalid token
310
- headers = self.create_auth_headers("api_key", token="invalid-token-999")
311
- data = {
312
- "jsonrpc": "2.0",
313
- "method": "echo",
314
- "params": {"message": "Should fail"},
315
- "id": 3
316
- }
317
- async with self.session.post(f"{server_url}/cmd",
318
- headers=headers,
319
- json=data) as response:
320
- duration = time.time() - start_time
321
- # Expected to fail with 401
322
- if response.status == 401:
323
- return TestResult(
324
- test_name=test_name,
325
- server_url=server_url,
326
- auth_type=auth_type,
327
- success=True, # This is expected to fail
328
- status_code=response.status,
329
- response_data={"expected": "authentication_failure"},
330
- duration=duration
331
- )
332
- else:
388
+ if auth_type == "certificate":
389
+ # For mTLS, test with invalid/expired certificate or no certificate
390
+ import aiohttp
391
+ from aiohttp import ClientTimeout, TCPConnector
392
+ import ssl
393
+
394
+ # Create SSL context with wrong certificate (should be rejected)
395
+ ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
396
+ ssl_context.check_hostname = False
397
+ # Don't load any client certificate - this should cause rejection
398
+ # Load CA certificate for server verification
399
+ ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
400
+ if os.path.exists(ca_cert_file):
401
+ ssl_context.load_verify_locations(cafile=ca_cert_file)
402
+ ssl_context.verify_mode = ssl.CERT_NONE # Don't verify server cert for testing
403
+
404
+ connector = TCPConnector(ssl=ssl_context)
405
+ timeout = ClientTimeout(total=10) # Shorter timeout
406
+
407
+ try:
408
+ async with aiohttp.ClientSession(timeout=timeout, connector=connector) as temp_session:
409
+ data = {
410
+ "jsonrpc": "2.0",
411
+ "method": "echo",
412
+ "params": {"message": "Should fail without certificate"},
413
+ "id": 3
414
+ }
415
+ async with temp_session.post(f"{server_url}/cmd", json=data) as response:
416
+ duration = time.time() - start_time
417
+ # If we get here, the server accepted the connection without proper certificate
418
+ # This is actually a security issue - server should reject
419
+ return TestResult(
420
+ test_name=test_name,
421
+ server_url=server_url,
422
+ auth_type=auth_type,
423
+ success=False,
424
+ status_code=response.status,
425
+ error_message=f"SECURITY ISSUE: mTLS server accepted connection without client certificate (status: {response.status})",
426
+ duration=duration
427
+ )
428
+ except (aiohttp.ClientError, aiohttp.ServerDisconnectedError, asyncio.TimeoutError) as e:
429
+ # This is expected - server should reject connections without proper certificate
430
+ duration = time.time() - start_time
333
431
  return TestResult(
334
432
  test_name=test_name,
335
433
  server_url=server_url,
336
434
  auth_type=auth_type,
337
- success=False,
338
- status_code=response.status,
339
- error_message=f"Expected 401, got {response.status}",
435
+ success=True,
436
+ status_code=0,
437
+ response_data={"expected": "connection_rejected", "error": str(e)},
340
438
  duration=duration
341
439
  )
440
+ else:
441
+ # For other auth types, use invalid token
442
+ headers = self.create_auth_headers("api_key", token="invalid-token-999")
443
+ data = {
444
+ "jsonrpc": "2.0",
445
+ "method": "echo",
446
+ "params": {"message": "Should fail"},
447
+ "id": 3
448
+ }
449
+ async with self.session.post(f"{server_url}/cmd",
450
+ headers=headers,
451
+ json=data) as response:
452
+ duration = time.time() - start_time
453
+ # Expect 401 only when auth is enforced
454
+ expects_auth = auth_type in ("api_key", "certificate", "basic")
455
+ if expects_auth and response.status == 401:
456
+ return TestResult(
457
+ test_name=test_name,
458
+ server_url=server_url,
459
+ auth_type=auth_type,
460
+ success=True,
461
+ status_code=response.status,
462
+ response_data={"expected": "authentication_failure"},
463
+ duration=duration
464
+ )
465
+ elif not expects_auth and response.status == 200:
466
+ # Security disabled: negative auth should not fail
467
+ return TestResult(
468
+ test_name=test_name,
469
+ server_url=server_url,
470
+ auth_type=auth_type,
471
+ success=True,
472
+ status_code=response.status,
473
+ response_data={"expected": "no_auth_required"},
474
+ duration=duration
475
+ )
476
+ else:
477
+ return TestResult(
478
+ test_name=test_name,
479
+ server_url=server_url,
480
+ auth_type=auth_type,
481
+ success=False,
482
+ status_code=response.status,
483
+ error_message=f"Unexpected status for negative auth: {response.status}",
484
+ duration=duration
485
+ )
342
486
  except Exception as e:
343
487
  duration = time.time() - start_time
344
488
  return TestResult(
mcp_proxy_adapter/main.py CHANGED
@@ -7,6 +7,7 @@ email: vasilyvz@gmail.com
7
7
  """
8
8
 
9
9
  import sys
10
+ import ssl
10
11
  import hypercorn.asyncio
11
12
  import hypercorn.config
12
13
  import asyncio
@@ -32,7 +33,10 @@ def main():
32
33
  config = Config(config_path=args.config)
33
34
  else:
34
35
  config = Config()
35
-
36
+
37
+ print(f"DEBUG main.py: config type: {type(config)}")
38
+ print(f"DEBUG main.py: config.get_all() type: {type(config.get_all())}")
39
+
36
40
  # Create application
37
41
  app = create_app(app_config=config)
38
42
 
@@ -70,8 +74,14 @@ def main():
70
74
  config_hypercorn.ca_certs = ssl_ca_cert
71
75
 
72
76
  if verify_client:
73
- import ssl
77
+ # For mTLS, require client certificates
78
+ config_hypercorn.set_cert_reqs(ssl.CERT_REQUIRED)
74
79
  config_hypercorn.verify_mode = ssl.CERT_REQUIRED
80
+ print("🔐 mTLS: Client certificate verification enabled")
81
+ else:
82
+ # For regular HTTPS without client verification
83
+ config_hypercorn.set_cert_reqs(ssl.CERT_NONE)
84
+ config_hypercorn.verify_mode = ssl.CERT_NONE
75
85
 
76
86
  print(f"🔐 Starting HTTPS server with hypercorn...")
77
87
  else: