mcp-proxy-adapter 6.2.21__py3-none-any.whl → 6.2.23__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/api/app.py +3 -0
- mcp_proxy_adapter/api/middleware/__init__.py +7 -7
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +59 -12
- mcp_proxy_adapter/api/middleware/unified_security.py +42 -6
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +99 -24
- mcp_proxy_adapter/core/protocol_manager.py +33 -5
- mcp_proxy_adapter/examples/create_certificates_simple.py +77 -9
- mcp_proxy_adapter/examples/generate_test_configs.py +44 -23
- mcp_proxy_adapter/examples/run_full_test_suite.py +125 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +9 -7
- mcp_proxy_adapter/examples/run_security_tests.py +187 -36
- mcp_proxy_adapter/examples/security_test_client.py +182 -38
- mcp_proxy_adapter/examples/setup_test_environment.py +12 -0
- mcp_proxy_adapter/main.py +12 -2
- mcp_proxy_adapter/utils/config_generator.py +740 -0
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.2.21.dist-info → mcp_proxy_adapter-6.2.23.dist-info}/METADATA +9 -11
- {mcp_proxy_adapter-6.2.21.dist-info → mcp_proxy_adapter-6.2.23.dist-info}/RECORD +22 -19
- mcp_proxy_adapter-6.2.23.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-6.2.21.dist-info → mcp_proxy_adapter-6.2.23.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.2.21.dist-info → mcp_proxy_adapter-6.2.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-6.2.21.dist-info → mcp_proxy_adapter-6.2.23.dist-info}/top_level.txt +0 -0
@@ -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
|
-
|
49
|
-
#
|
50
|
-
self.security_manager = None
|
69
|
+
|
70
|
+
# Initialize security managers if available
|
51
71
|
self.ssl_manager = None
|
52
|
-
self.
|
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
|
-
|
89
|
-
|
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/
|
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")
|
125
|
-
|
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
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
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=
|
338
|
-
status_code=
|
339
|
-
|
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(
|
@@ -99,6 +99,18 @@ def setup_test_environment(output_dir: Path) -> None:
|
|
99
99
|
if cert_tokens_src.exists():
|
100
100
|
shutil.copy2(cert_tokens_src, output_dir / "scripts/")
|
101
101
|
print("✅ Copied generate_certificates_and_tokens.py")
|
102
|
+
|
103
|
+
# Copy roles.json to the root directory for compatibility
|
104
|
+
roles_src = examples_src_root / "roles.json"
|
105
|
+
if roles_src.exists():
|
106
|
+
shutil.copy2(roles_src, output_dir)
|
107
|
+
print("✅ Copied roles.json to root directory")
|
108
|
+
|
109
|
+
# Also copy from configs directory if it exists
|
110
|
+
roles_configs_src = output_dir / "configs" / "roles.json"
|
111
|
+
if roles_configs_src.exists():
|
112
|
+
shutil.copy2(roles_configs_src, output_dir / "roles.json")
|
113
|
+
print("✅ Updated roles.json from configs directory")
|
102
114
|
print(
|
103
115
|
"🎉 Test environment setup completed successfully at: {}".format(
|
104
116
|
output_dir
|
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
|
-
|
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:
|