mcp-proxy-adapter 6.3.3__py3-none-any.whl ā 6.3.5__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/__init__.py +9 -5
- mcp_proxy_adapter/__main__.py +1 -1
- mcp_proxy_adapter/api/app.py +227 -176
- mcp_proxy_adapter/api/handlers.py +68 -60
- mcp_proxy_adapter/api/middleware/__init__.py +7 -5
- mcp_proxy_adapter/api/middleware/base.py +19 -16
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +44 -34
- mcp_proxy_adapter/api/middleware/error_handling.py +57 -67
- mcp_proxy_adapter/api/middleware/factory.py +50 -52
- mcp_proxy_adapter/api/middleware/logging.py +46 -30
- mcp_proxy_adapter/api/middleware/performance.py +19 -16
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +80 -50
- mcp_proxy_adapter/api/middleware/transport_middleware.py +26 -24
- mcp_proxy_adapter/api/middleware/unified_security.py +70 -51
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +43 -34
- mcp_proxy_adapter/api/schemas.py +69 -43
- mcp_proxy_adapter/api/tool_integration.py +83 -63
- mcp_proxy_adapter/api/tools.py +60 -50
- mcp_proxy_adapter/commands/__init__.py +15 -6
- mcp_proxy_adapter/commands/auth_validation_command.py +107 -110
- mcp_proxy_adapter/commands/base.py +108 -112
- mcp_proxy_adapter/commands/builtin_commands.py +28 -18
- mcp_proxy_adapter/commands/catalog_manager.py +394 -265
- mcp_proxy_adapter/commands/cert_monitor_command.py +222 -204
- mcp_proxy_adapter/commands/certificate_management_command.py +210 -213
- mcp_proxy_adapter/commands/command_registry.py +275 -226
- mcp_proxy_adapter/commands/config_command.py +48 -33
- mcp_proxy_adapter/commands/dependency_container.py +22 -23
- mcp_proxy_adapter/commands/dependency_manager.py +65 -56
- mcp_proxy_adapter/commands/echo_command.py +15 -15
- mcp_proxy_adapter/commands/health_command.py +31 -29
- mcp_proxy_adapter/commands/help_command.py +97 -61
- mcp_proxy_adapter/commands/hooks.py +65 -49
- mcp_proxy_adapter/commands/key_management_command.py +148 -147
- mcp_proxy_adapter/commands/load_command.py +58 -40
- mcp_proxy_adapter/commands/plugins_command.py +80 -54
- mcp_proxy_adapter/commands/protocol_management_command.py +60 -48
- mcp_proxy_adapter/commands/proxy_registration_command.py +107 -115
- mcp_proxy_adapter/commands/reload_command.py +43 -37
- mcp_proxy_adapter/commands/result.py +26 -33
- mcp_proxy_adapter/commands/role_test_command.py +26 -26
- mcp_proxy_adapter/commands/roles_management_command.py +176 -173
- mcp_proxy_adapter/commands/security_command.py +134 -122
- mcp_proxy_adapter/commands/settings_command.py +47 -56
- mcp_proxy_adapter/commands/ssl_setup_command.py +109 -129
- mcp_proxy_adapter/commands/token_management_command.py +129 -158
- mcp_proxy_adapter/commands/transport_management_command.py +41 -36
- mcp_proxy_adapter/commands/unload_command.py +42 -37
- mcp_proxy_adapter/config.py +36 -35
- mcp_proxy_adapter/core/__init__.py +19 -21
- mcp_proxy_adapter/core/app_factory.py +30 -9
- mcp_proxy_adapter/core/app_runner.py +81 -64
- mcp_proxy_adapter/core/auth_validator.py +176 -182
- mcp_proxy_adapter/core/certificate_utils.py +469 -426
- mcp_proxy_adapter/core/client.py +155 -126
- mcp_proxy_adapter/core/client_manager.py +60 -54
- mcp_proxy_adapter/core/client_security.py +108 -88
- mcp_proxy_adapter/core/config_converter.py +176 -143
- mcp_proxy_adapter/core/config_validator.py +12 -4
- mcp_proxy_adapter/core/crl_utils.py +21 -7
- mcp_proxy_adapter/core/errors.py +64 -20
- mcp_proxy_adapter/core/logging.py +34 -29
- mcp_proxy_adapter/core/mtls_asgi.py +29 -25
- mcp_proxy_adapter/core/mtls_asgi_app.py +66 -54
- mcp_proxy_adapter/core/protocol_manager.py +154 -104
- mcp_proxy_adapter/core/proxy_client.py +202 -144
- mcp_proxy_adapter/core/proxy_registration.py +12 -2
- mcp_proxy_adapter/core/role_utils.py +139 -125
- mcp_proxy_adapter/core/security_adapter.py +88 -77
- mcp_proxy_adapter/core/security_factory.py +50 -44
- mcp_proxy_adapter/core/security_integration.py +72 -24
- mcp_proxy_adapter/core/server_adapter.py +68 -64
- mcp_proxy_adapter/core/server_engine.py +71 -53
- mcp_proxy_adapter/core/settings.py +68 -58
- mcp_proxy_adapter/core/ssl_utils.py +69 -56
- mcp_proxy_adapter/core/transport_manager.py +72 -60
- mcp_proxy_adapter/core/unified_config_adapter.py +201 -150
- mcp_proxy_adapter/core/utils.py +4 -2
- mcp_proxy_adapter/custom_openapi.py +107 -99
- mcp_proxy_adapter/examples/basic_framework/main.py +9 -2
- mcp_proxy_adapter/examples/commands/__init__.py +1 -1
- mcp_proxy_adapter/examples/create_certificates_simple.py +182 -71
- mcp_proxy_adapter/examples/debug_request_state.py +38 -19
- mcp_proxy_adapter/examples/debug_role_chain.py +53 -20
- mcp_proxy_adapter/examples/demo_client.py +48 -36
- mcp_proxy_adapter/examples/examples/basic_framework/main.py +9 -2
- mcp_proxy_adapter/examples/examples/full_application/__init__.py +1 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +22 -10
- mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +24 -17
- mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +16 -3
- mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +13 -3
- mcp_proxy_adapter/examples/examples/full_application/main.py +27 -2
- mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +48 -14
- mcp_proxy_adapter/examples/full_application/__init__.py +1 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +22 -10
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +24 -17
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +16 -3
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +13 -3
- mcp_proxy_adapter/examples/full_application/main.py +27 -2
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +48 -14
- mcp_proxy_adapter/examples/generate_all_certificates.py +198 -73
- mcp_proxy_adapter/examples/generate_certificates.py +31 -16
- mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +220 -74
- mcp_proxy_adapter/examples/generate_test_configs.py +68 -91
- mcp_proxy_adapter/examples/proxy_registration_example.py +76 -75
- mcp_proxy_adapter/examples/run_example.py +23 -5
- mcp_proxy_adapter/examples/run_full_test_suite.py +109 -71
- mcp_proxy_adapter/examples/run_proxy_server.py +22 -9
- mcp_proxy_adapter/examples/run_security_tests.py +103 -41
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +72 -36
- mcp_proxy_adapter/examples/scripts/config_generator.py +288 -187
- mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +185 -72
- mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +220 -74
- mcp_proxy_adapter/examples/security_test_client.py +196 -127
- mcp_proxy_adapter/examples/setup_test_environment.py +17 -29
- mcp_proxy_adapter/examples/test_config.py +19 -4
- mcp_proxy_adapter/examples/test_config_generator.py +23 -7
- mcp_proxy_adapter/examples/test_examples.py +84 -56
- mcp_proxy_adapter/examples/universal_client.py +119 -62
- mcp_proxy_adapter/openapi.py +108 -115
- mcp_proxy_adapter/utils/config_generator.py +429 -274
- mcp_proxy_adapter/version.py +1 -2
- {mcp_proxy_adapter-6.3.3.dist-info ā mcp_proxy_adapter-6.3.5.dist-info}/METADATA +1 -1
- mcp_proxy_adapter-6.3.5.dist-info/RECORD +143 -0
- mcp_proxy_adapter-6.3.3.dist-info/RECORD +0 -143
- {mcp_proxy_adapter-6.3.3.dist-info ā mcp_proxy_adapter-6.3.5.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.3.3.dist-info ā mcp_proxy_adapter-6.3.5.dist-info}/entry_points.txt +0 -0
- {mcp_proxy_adapter-6.3.3.dist-info ā mcp_proxy_adapter-6.3.5.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-6.3.3.dist-info ā mcp_proxy_adapter-6.3.5.dist-info}/top_level.txt +0 -0
@@ -34,6 +34,7 @@ sys.path.insert(0, str(parent_dir))
|
|
34
34
|
try:
|
35
35
|
from mcp_security_framework import SSLManager, CertificateManager
|
36
36
|
from mcp_security_framework.schemas.config import SSLConfig
|
37
|
+
|
37
38
|
_MCP_SECURITY_AVAILABLE = True
|
38
39
|
print("ā
mcp_security_framework available")
|
39
40
|
except ImportError:
|
@@ -44,14 +45,18 @@ except ImportError:
|
|
44
45
|
try:
|
45
46
|
from cryptography import x509
|
46
47
|
from cryptography.hazmat.primitives import serialization
|
48
|
+
|
47
49
|
_CRYPTOGRAPHY_AVAILABLE = True
|
48
50
|
print("ā
cryptography available")
|
49
51
|
except ImportError:
|
50
52
|
_CRYPTOGRAPHY_AVAILABLE = False
|
51
53
|
print("ā ļø cryptography not available, SSL validation will be limited")
|
54
|
+
|
55
|
+
|
52
56
|
@dataclass
|
53
57
|
class TestResult:
|
54
58
|
"""Test result data class."""
|
59
|
+
|
55
60
|
test_name: str
|
56
61
|
server_url: str
|
57
62
|
auth_type: str
|
@@ -60,8 +65,11 @@ class TestResult:
|
|
60
65
|
response_data: Optional[Dict] = None
|
61
66
|
error_message: Optional[str] = None
|
62
67
|
duration: float = 0.0
|
68
|
+
|
69
|
+
|
63
70
|
class SecurityTestClient:
|
64
71
|
"""Security test client for comprehensive testing."""
|
72
|
+
|
65
73
|
def __init__(self, base_url: str = "http://localhost:8000"):
|
66
74
|
"""Initialize security test client."""
|
67
75
|
self.base_url = base_url
|
@@ -82,7 +90,7 @@ class SecurityTestClient:
|
|
82
90
|
key_file=None,
|
83
91
|
ca_cert_file=None,
|
84
92
|
verify_mode="CERT_NONE", # For testing
|
85
|
-
min_tls_version="TLSv1.2"
|
93
|
+
min_tls_version="TLSv1.2",
|
86
94
|
)
|
87
95
|
self.ssl_manager = SSLManager(ssl_config)
|
88
96
|
print("ā
SSL Manager initialized with mcp_security_framework")
|
@@ -101,23 +109,24 @@ class SecurityTestClient:
|
|
101
109
|
"readonly": "readonly-token-123",
|
102
110
|
"guest": "guest-token-123",
|
103
111
|
"proxy": "proxy-token-123",
|
104
|
-
"invalid": "invalid-token-999"
|
112
|
+
"invalid": "invalid-token-999",
|
105
113
|
}
|
106
114
|
# Test certificates
|
107
115
|
self.test_certificates = {
|
108
116
|
"admin": {
|
109
117
|
"cert": "mcp_proxy_adapter/examples/certs/admin_cert.pem",
|
110
|
-
"key": "mcp_proxy_adapter/examples/certs/admin_key.pem"
|
118
|
+
"key": "mcp_proxy_adapter/examples/certs/admin_key.pem",
|
111
119
|
},
|
112
120
|
"user": {
|
113
121
|
"cert": "mcp_proxy_adapter/examples/certs/user_cert.pem",
|
114
|
-
"key": "mcp_proxy_adapter/examples/certs/user_key.pem"
|
122
|
+
"key": "mcp_proxy_adapter/examples/certs/user_key.pem",
|
115
123
|
},
|
116
124
|
"readonly": {
|
117
125
|
"cert": "mcp_proxy_adapter/examples/certs/readonly_cert.pem",
|
118
|
-
"key": "mcp_proxy_adapter/examples/certs/readonly_key.pem"
|
119
|
-
}
|
126
|
+
"key": "mcp_proxy_adapter/examples/certs/readonly_key.pem",
|
127
|
+
},
|
120
128
|
}
|
129
|
+
|
121
130
|
async def __aenter__(self):
|
122
131
|
"""Async context manager entry."""
|
123
132
|
timeout = ClientTimeout(total=30)
|
@@ -126,6 +135,7 @@ class SecurityTestClient:
|
|
126
135
|
connector = TCPConnector(ssl=ssl_context)
|
127
136
|
self.session = ClientSession(timeout=timeout, connector=connector)
|
128
137
|
return self
|
138
|
+
|
129
139
|
def create_ssl_context_for_mtls(self) -> ssl.SSLContext:
|
130
140
|
"""Create SSL context for mTLS connections."""
|
131
141
|
if self.ssl_manager and self._security_available:
|
@@ -140,10 +150,12 @@ class SecurityTestClient:
|
|
140
150
|
client_cert_file=cert_file if os.path.exists(cert_file) else None,
|
141
151
|
client_key_file=key_file if os.path.exists(key_file) else None,
|
142
152
|
verify_mode="CERT_NONE", # For testing
|
143
|
-
min_version="TLSv1.2"
|
153
|
+
min_version="TLSv1.2",
|
144
154
|
)
|
145
155
|
except Exception as e:
|
146
|
-
print(
|
156
|
+
print(
|
157
|
+
f"ā ļø Failed to create mTLS context with mcp_security_framework: {e}"
|
158
|
+
)
|
147
159
|
print("ā¹ļø Falling back to standard SSL")
|
148
160
|
|
149
161
|
# Fallback to standard SSL
|
@@ -160,26 +172,41 @@ class SecurityTestClient:
|
|
160
172
|
if os.path.exists(ca_cert_file):
|
161
173
|
ssl_context.load_verify_locations(cafile=ca_cert_file)
|
162
174
|
return ssl_context
|
175
|
+
|
163
176
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
164
177
|
"""Async context manager exit."""
|
165
178
|
if self.session:
|
166
179
|
await self.session.close()
|
167
|
-
|
168
|
-
|
169
|
-
|
180
|
+
|
181
|
+
def create_ssl_context(
|
182
|
+
self,
|
183
|
+
cert_file: Optional[str] = None,
|
184
|
+
key_file: Optional[str] = None,
|
185
|
+
ca_cert_file: Optional[str] = None,
|
186
|
+
) -> ssl.SSLContext:
|
170
187
|
"""Create SSL context for client."""
|
171
188
|
if self.ssl_manager and self._security_available:
|
172
189
|
try:
|
173
190
|
# Use mcp_security_framework for SSL context creation
|
174
191
|
return self.ssl_manager.create_client_context(
|
175
|
-
ca_cert_file=
|
176
|
-
|
177
|
-
|
192
|
+
ca_cert_file=(
|
193
|
+
ca_cert_file
|
194
|
+
if ca_cert_file and os.path.exists(ca_cert_file)
|
195
|
+
else None
|
196
|
+
),
|
197
|
+
client_cert_file=(
|
198
|
+
cert_file if cert_file and os.path.exists(cert_file) else None
|
199
|
+
),
|
200
|
+
client_key_file=(
|
201
|
+
key_file if key_file and os.path.exists(key_file) else None
|
202
|
+
),
|
178
203
|
verify_mode="CERT_NONE", # For testing
|
179
|
-
min_version="TLSv1.2"
|
204
|
+
min_version="TLSv1.2",
|
180
205
|
)
|
181
206
|
except Exception as e:
|
182
|
-
print(
|
207
|
+
print(
|
208
|
+
f"ā ļø Failed to create SSL context with mcp_security_framework: {e}"
|
209
|
+
)
|
183
210
|
print("ā¹ļø Falling back to standard SSL")
|
184
211
|
|
185
212
|
# Fallback to standard SSL
|
@@ -194,6 +221,7 @@ class SecurityTestClient:
|
|
194
221
|
# For testing, still don't verify
|
195
222
|
ssl_context.verify_mode = ssl.CERT_NONE
|
196
223
|
return ssl_context
|
224
|
+
|
197
225
|
def create_auth_headers(self, auth_type: str, **kwargs) -> Dict[str, str]:
|
198
226
|
"""Create authentication headers."""
|
199
227
|
headers = {"Content-Type": "application/json"}
|
@@ -206,6 +234,7 @@ class SecurityTestClient:
|
|
206
234
|
username = kwargs.get("username", "admin")
|
207
235
|
password = kwargs.get("password", "password")
|
208
236
|
import base64
|
237
|
+
|
209
238
|
credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
|
210
239
|
headers["Authorization"] = f"Basic {credentials}"
|
211
240
|
elif auth_type == "certificate":
|
@@ -213,13 +242,18 @@ class SecurityTestClient:
|
|
213
242
|
# This is handled by SSL context, not headers
|
214
243
|
pass
|
215
244
|
return headers
|
216
|
-
|
245
|
+
|
246
|
+
async def test_health_check(
|
247
|
+
self, server_url: str, auth_type: str = "none", **kwargs
|
248
|
+
) -> TestResult:
|
217
249
|
"""Test health check endpoint."""
|
218
250
|
start_time = time.time()
|
219
251
|
test_name = f"Health Check ({auth_type})"
|
220
252
|
try:
|
221
253
|
headers = self.create_auth_headers(auth_type, **kwargs)
|
222
|
-
async with self.session.get(
|
254
|
+
async with self.session.get(
|
255
|
+
f"{server_url}/health", headers=headers
|
256
|
+
) as response:
|
223
257
|
duration = time.time() - start_time
|
224
258
|
if response.status == 200:
|
225
259
|
data = await response.json()
|
@@ -230,7 +264,7 @@ class SecurityTestClient:
|
|
230
264
|
success=True,
|
231
265
|
status_code=response.status,
|
232
266
|
response_data=data,
|
233
|
-
duration=duration
|
267
|
+
duration=duration,
|
234
268
|
)
|
235
269
|
else:
|
236
270
|
error_text = await response.text()
|
@@ -241,7 +275,7 @@ class SecurityTestClient:
|
|
241
275
|
success=False,
|
242
276
|
status_code=response.status,
|
243
277
|
error_message=f"Health check failed: {error_text}",
|
244
|
-
duration=duration
|
278
|
+
duration=duration,
|
245
279
|
)
|
246
280
|
except Exception as e:
|
247
281
|
duration = time.time() - start_time
|
@@ -251,9 +285,12 @@ class SecurityTestClient:
|
|
251
285
|
auth_type=auth_type,
|
252
286
|
success=False,
|
253
287
|
error_message=f"Health check error: {str(e)}",
|
254
|
-
duration=duration
|
288
|
+
duration=duration,
|
255
289
|
)
|
256
|
-
|
290
|
+
|
291
|
+
async def test_echo_command(
|
292
|
+
self, server_url: str, auth_type: str = "none", **kwargs
|
293
|
+
) -> TestResult:
|
257
294
|
"""Test echo command."""
|
258
295
|
start_time = time.time()
|
259
296
|
test_name = f"Echo Command ({auth_type})"
|
@@ -262,14 +299,12 @@ class SecurityTestClient:
|
|
262
299
|
data = {
|
263
300
|
"jsonrpc": "2.0",
|
264
301
|
"method": "echo",
|
265
|
-
"params": {
|
266
|
-
|
267
|
-
},
|
268
|
-
"id": 1
|
302
|
+
"params": {"message": "Hello from security test client!"},
|
303
|
+
"id": 1,
|
269
304
|
}
|
270
|
-
async with self.session.post(
|
271
|
-
|
272
|
-
|
305
|
+
async with self.session.post(
|
306
|
+
f"{server_url}/cmd", headers=headers, json=data
|
307
|
+
) as response:
|
273
308
|
duration = time.time() - start_time
|
274
309
|
if response.status == 200:
|
275
310
|
data = await response.json()
|
@@ -280,7 +315,7 @@ class SecurityTestClient:
|
|
280
315
|
success=True,
|
281
316
|
status_code=response.status,
|
282
317
|
response_data=data,
|
283
|
-
duration=duration
|
318
|
+
duration=duration,
|
284
319
|
)
|
285
320
|
else:
|
286
321
|
error_text = await response.text()
|
@@ -291,7 +326,7 @@ class SecurityTestClient:
|
|
291
326
|
success=False,
|
292
327
|
status_code=response.status,
|
293
328
|
error_message=f"Echo command failed: {error_text}",
|
294
|
-
duration=duration
|
329
|
+
duration=duration,
|
295
330
|
)
|
296
331
|
except Exception as e:
|
297
332
|
duration = time.time() - start_time
|
@@ -301,23 +336,21 @@ class SecurityTestClient:
|
|
301
336
|
auth_type=auth_type,
|
302
337
|
success=False,
|
303
338
|
error_message=f"Echo command error: {str(e)}",
|
304
|
-
duration=duration
|
339
|
+
duration=duration,
|
305
340
|
)
|
306
|
-
|
341
|
+
|
342
|
+
async def test_security_command(
|
343
|
+
self, server_url: str, auth_type: str = "none", **kwargs
|
344
|
+
) -> TestResult:
|
307
345
|
"""Test security command."""
|
308
346
|
start_time = time.time()
|
309
347
|
test_name = f"Security Command ({auth_type})"
|
310
348
|
try:
|
311
349
|
headers = self.create_auth_headers(auth_type, **kwargs)
|
312
|
-
data = {
|
313
|
-
|
314
|
-
"
|
315
|
-
|
316
|
-
"id": 2
|
317
|
-
}
|
318
|
-
async with self.session.post(f"{server_url}/cmd",
|
319
|
-
headers=headers,
|
320
|
-
json=data) as response:
|
350
|
+
data = {"jsonrpc": "2.0", "method": "health", "params": {}, "id": 2}
|
351
|
+
async with self.session.post(
|
352
|
+
f"{server_url}/cmd", headers=headers, json=data
|
353
|
+
) as response:
|
321
354
|
duration = time.time() - start_time
|
322
355
|
if response.status == 200:
|
323
356
|
data = await response.json()
|
@@ -328,7 +361,7 @@ class SecurityTestClient:
|
|
328
361
|
success=True,
|
329
362
|
status_code=response.status,
|
330
363
|
response_data=data,
|
331
|
-
duration=duration
|
364
|
+
duration=duration,
|
332
365
|
)
|
333
366
|
else:
|
334
367
|
error_text = await response.text()
|
@@ -339,7 +372,7 @@ class SecurityTestClient:
|
|
339
372
|
success=False,
|
340
373
|
status_code=response.status,
|
341
374
|
error_message=f"Security command failed: {error_text}",
|
342
|
-
duration=duration
|
375
|
+
duration=duration,
|
343
376
|
)
|
344
377
|
except Exception as e:
|
345
378
|
duration = time.time() - start_time
|
@@ -349,14 +382,17 @@ class SecurityTestClient:
|
|
349
382
|
auth_type=auth_type,
|
350
383
|
success=False,
|
351
384
|
error_message=f"Security command error: {str(e)}",
|
352
|
-
duration=duration
|
385
|
+
duration=duration,
|
353
386
|
)
|
387
|
+
|
354
388
|
async def test_health(self) -> TestResult:
|
355
389
|
"""Test health endpoint."""
|
356
390
|
return await self.test_health_check(self.base_url, "none")
|
391
|
+
|
357
392
|
async def test_command_execution(self) -> TestResult:
|
358
393
|
"""Test command execution."""
|
359
394
|
return await self.test_echo_command(self.base_url, "none")
|
395
|
+
|
360
396
|
async def test_authentication(self) -> TestResult:
|
361
397
|
"""Test authentication."""
|
362
398
|
if "api_key" in self.auth_methods:
|
@@ -372,15 +408,22 @@ class SecurityTestClient:
|
|
372
408
|
server_url=self.base_url,
|
373
409
|
auth_type="none",
|
374
410
|
success=False,
|
375
|
-
error_message="No authentication method available"
|
411
|
+
error_message="No authentication method available",
|
376
412
|
)
|
413
|
+
|
377
414
|
async def test_negative_authentication(self) -> TestResult:
|
378
415
|
"""Test negative authentication (should fail)."""
|
379
|
-
return await self.test_echo_command(
|
416
|
+
return await self.test_echo_command(
|
417
|
+
self.base_url, "api_key", token="invalid-token"
|
418
|
+
)
|
419
|
+
|
380
420
|
async def test_no_auth_required(self) -> TestResult:
|
381
421
|
"""Test that no authentication is required."""
|
382
422
|
return await self.test_echo_command(self.base_url, "none")
|
383
|
-
|
423
|
+
|
424
|
+
async def test_negative_auth(
|
425
|
+
self, server_url: str, auth_type: str = "none", **kwargs
|
426
|
+
) -> TestResult:
|
384
427
|
"""Test negative authentication scenarios."""
|
385
428
|
start_time = time.time()
|
386
429
|
test_name = f"Negative Auth ({auth_type})"
|
@@ -399,20 +442,26 @@ class SecurityTestClient:
|
|
399
442
|
ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
|
400
443
|
if os.path.exists(ca_cert_file):
|
401
444
|
ssl_context.load_verify_locations(cafile=ca_cert_file)
|
402
|
-
ssl_context.verify_mode =
|
445
|
+
ssl_context.verify_mode = (
|
446
|
+
ssl.CERT_NONE
|
447
|
+
) # Don't verify server cert for testing
|
403
448
|
|
404
449
|
connector = TCPConnector(ssl=ssl_context)
|
405
450
|
timeout = ClientTimeout(total=10) # Shorter timeout
|
406
451
|
|
407
452
|
try:
|
408
|
-
async with aiohttp.ClientSession(
|
453
|
+
async with aiohttp.ClientSession(
|
454
|
+
timeout=timeout, connector=connector
|
455
|
+
) as temp_session:
|
409
456
|
data = {
|
410
457
|
"jsonrpc": "2.0",
|
411
458
|
"method": "echo",
|
412
459
|
"params": {"message": "Should fail without certificate"},
|
413
|
-
"id": 3
|
460
|
+
"id": 3,
|
414
461
|
}
|
415
|
-
async with temp_session.post(
|
462
|
+
async with temp_session.post(
|
463
|
+
f"{server_url}/cmd", json=data
|
464
|
+
) as response:
|
416
465
|
duration = time.time() - start_time
|
417
466
|
# If we get here, the server accepted the connection without proper certificate
|
418
467
|
# This is actually a security issue - server should reject
|
@@ -423,9 +472,13 @@ class SecurityTestClient:
|
|
423
472
|
success=False,
|
424
473
|
status_code=response.status,
|
425
474
|
error_message=f"SECURITY ISSUE: mTLS server accepted connection without client certificate (status: {response.status})",
|
426
|
-
duration=duration
|
475
|
+
duration=duration,
|
427
476
|
)
|
428
|
-
except (
|
477
|
+
except (
|
478
|
+
aiohttp.ClientError,
|
479
|
+
aiohttp.ServerDisconnectedError,
|
480
|
+
asyncio.TimeoutError,
|
481
|
+
) as e:
|
429
482
|
# This is expected - server should reject connections without proper certificate
|
430
483
|
duration = time.time() - start_time
|
431
484
|
return TestResult(
|
@@ -434,8 +487,11 @@ class SecurityTestClient:
|
|
434
487
|
auth_type=auth_type,
|
435
488
|
success=True,
|
436
489
|
status_code=0,
|
437
|
-
response_data={
|
438
|
-
|
490
|
+
response_data={
|
491
|
+
"expected": "connection_rejected",
|
492
|
+
"error": str(e),
|
493
|
+
},
|
494
|
+
duration=duration,
|
439
495
|
)
|
440
496
|
else:
|
441
497
|
# For other auth types, use invalid token
|
@@ -444,11 +500,11 @@ class SecurityTestClient:
|
|
444
500
|
"jsonrpc": "2.0",
|
445
501
|
"method": "echo",
|
446
502
|
"params": {"message": "Should fail"},
|
447
|
-
"id": 3
|
503
|
+
"id": 3,
|
448
504
|
}
|
449
|
-
async with self.session.post(
|
450
|
-
|
451
|
-
|
505
|
+
async with self.session.post(
|
506
|
+
f"{server_url}/cmd", headers=headers, json=data
|
507
|
+
) as response:
|
452
508
|
duration = time.time() - start_time
|
453
509
|
# Expect 401 only when auth is enforced
|
454
510
|
expects_auth = auth_type in ("api_key", "certificate", "basic")
|
@@ -460,7 +516,7 @@ class SecurityTestClient:
|
|
460
516
|
success=True,
|
461
517
|
status_code=response.status,
|
462
518
|
response_data={"expected": "authentication_failure"},
|
463
|
-
duration=duration
|
519
|
+
duration=duration,
|
464
520
|
)
|
465
521
|
elif not expects_auth and response.status == 200:
|
466
522
|
# Security disabled: negative auth should not fail
|
@@ -471,7 +527,7 @@ class SecurityTestClient:
|
|
471
527
|
success=True,
|
472
528
|
status_code=response.status,
|
473
529
|
response_data={"expected": "no_auth_required"},
|
474
|
-
duration=duration
|
530
|
+
duration=duration,
|
475
531
|
)
|
476
532
|
else:
|
477
533
|
return TestResult(
|
@@ -481,7 +537,7 @@ class SecurityTestClient:
|
|
481
537
|
success=False,
|
482
538
|
status_code=response.status,
|
483
539
|
error_message=f"Unexpected status for negative auth: {response.status}",
|
484
|
-
duration=duration
|
540
|
+
duration=duration,
|
485
541
|
)
|
486
542
|
except Exception as e:
|
487
543
|
duration = time.time() - start_time
|
@@ -491,9 +547,12 @@ class SecurityTestClient:
|
|
491
547
|
auth_type=auth_type,
|
492
548
|
success=False,
|
493
549
|
error_message=f"Negative auth error: {str(e)}",
|
494
|
-
duration=duration
|
550
|
+
duration=duration,
|
495
551
|
)
|
496
|
-
|
552
|
+
|
553
|
+
async def test_role_based_access(
|
554
|
+
self, server_url: str, auth_type: str = "none", **kwargs
|
555
|
+
) -> TestResult:
|
497
556
|
"""Test role-based access control."""
|
498
557
|
start_time = time.time()
|
499
558
|
test_name = f"Role-Based Access ({auth_type})"
|
@@ -506,11 +565,11 @@ class SecurityTestClient:
|
|
506
565
|
"jsonrpc": "2.0",
|
507
566
|
"method": "echo",
|
508
567
|
"params": {"message": f"Testing {role} role"},
|
509
|
-
"id": 4
|
568
|
+
"id": 4,
|
510
569
|
}
|
511
|
-
async with self.session.post(
|
512
|
-
|
513
|
-
|
570
|
+
async with self.session.post(
|
571
|
+
f"{server_url}/cmd", headers=headers, json=data
|
572
|
+
) as response:
|
514
573
|
duration = time.time() - start_time
|
515
574
|
if response.status == 200:
|
516
575
|
data = await response.json()
|
@@ -521,7 +580,7 @@ class SecurityTestClient:
|
|
521
580
|
success=True,
|
522
581
|
status_code=response.status,
|
523
582
|
response_data=data,
|
524
|
-
duration=duration
|
583
|
+
duration=duration,
|
525
584
|
)
|
526
585
|
else:
|
527
586
|
error_text = await response.text()
|
@@ -532,7 +591,7 @@ class SecurityTestClient:
|
|
532
591
|
success=False,
|
533
592
|
status_code=response.status,
|
534
593
|
error_message=f"Role-based access failed: {error_text}",
|
535
|
-
duration=duration
|
594
|
+
duration=duration,
|
536
595
|
)
|
537
596
|
except Exception as e:
|
538
597
|
duration = time.time() - start_time
|
@@ -542,9 +601,12 @@ class SecurityTestClient:
|
|
542
601
|
auth_type=auth_type,
|
543
602
|
success=False,
|
544
603
|
error_message=f"Role-based access error: {str(e)}",
|
545
|
-
duration=duration
|
604
|
+
duration=duration,
|
546
605
|
)
|
547
|
-
|
606
|
+
|
607
|
+
async def test_role_permissions(
|
608
|
+
self, server_url: str, auth_type: str = "none", **kwargs
|
609
|
+
) -> TestResult:
|
548
610
|
"""Test role permissions with role_test command."""
|
549
611
|
start_time = time.time()
|
550
612
|
test_name = f"Role Permissions Test ({auth_type})"
|
@@ -558,11 +620,11 @@ class SecurityTestClient:
|
|
558
620
|
"jsonrpc": "2.0",
|
559
621
|
"method": "role_test",
|
560
622
|
"params": {"action": action},
|
561
|
-
"id": 5
|
623
|
+
"id": 5,
|
562
624
|
}
|
563
|
-
async with self.session.post(
|
564
|
-
|
565
|
-
|
625
|
+
async with self.session.post(
|
626
|
+
f"{server_url}/cmd", headers=headers, json=data
|
627
|
+
) as response:
|
566
628
|
duration = time.time() - start_time
|
567
629
|
if response.status == 200:
|
568
630
|
data = await response.json()
|
@@ -573,7 +635,7 @@ class SecurityTestClient:
|
|
573
635
|
success=True,
|
574
636
|
status_code=response.status,
|
575
637
|
response_data=data,
|
576
|
-
duration=duration
|
638
|
+
duration=duration,
|
577
639
|
)
|
578
640
|
else:
|
579
641
|
error_text = await response.text()
|
@@ -584,7 +646,7 @@ class SecurityTestClient:
|
|
584
646
|
success=False,
|
585
647
|
status_code=response.status,
|
586
648
|
error_message=f"Role permissions test failed: {error_text}",
|
587
|
-
duration=duration
|
649
|
+
duration=duration,
|
588
650
|
)
|
589
651
|
except Exception as e:
|
590
652
|
duration = time.time() - start_time
|
@@ -594,9 +656,12 @@ class SecurityTestClient:
|
|
594
656
|
auth_type=auth_type,
|
595
657
|
success=False,
|
596
658
|
error_message=f"Role permissions test error: {str(e)}",
|
597
|
-
duration=duration
|
659
|
+
duration=duration,
|
598
660
|
)
|
599
|
-
|
661
|
+
|
662
|
+
async def test_multiple_roles(
|
663
|
+
self, server_url: str, auth_type: str = "none", **kwargs
|
664
|
+
) -> TestResult:
|
600
665
|
"""Test multiple roles with different permissions."""
|
601
666
|
start_time = time.time()
|
602
667
|
test_name = f"Multiple Roles Test ({auth_type})"
|
@@ -608,11 +673,11 @@ class SecurityTestClient:
|
|
608
673
|
"jsonrpc": "2.0",
|
609
674
|
"method": "role_test",
|
610
675
|
"params": {"action": "manage"},
|
611
|
-
"id": 6
|
676
|
+
"id": 6,
|
612
677
|
}
|
613
|
-
async with self.session.post(
|
614
|
-
|
615
|
-
|
678
|
+
async with self.session.post(
|
679
|
+
f"{server_url}/cmd", headers=admin_headers, json=admin_data
|
680
|
+
) as response:
|
616
681
|
if response.status != 200:
|
617
682
|
return TestResult(
|
618
683
|
test_name=test_name,
|
@@ -621,19 +686,21 @@ class SecurityTestClient:
|
|
621
686
|
success=False,
|
622
687
|
status_code=response.status,
|
623
688
|
error_message="Admin role test failed",
|
624
|
-
duration=time.time() - start_time
|
689
|
+
duration=time.time() - start_time,
|
625
690
|
)
|
626
691
|
# Test readonly role (should only have read permission)
|
627
692
|
readonly_token = self.test_tokens.get("readonly", "readonly-token-123")
|
628
|
-
readonly_headers = self.create_auth_headers(
|
693
|
+
readonly_headers = self.create_auth_headers(
|
694
|
+
"api_key", token=readonly_token
|
695
|
+
)
|
629
696
|
readonly_data = {
|
630
697
|
"jsonrpc": "2.0",
|
631
698
|
"method": "role_test",
|
632
699
|
"params": {"action": "write"},
|
633
700
|
}
|
634
|
-
async with self.session.post(
|
635
|
-
|
636
|
-
|
701
|
+
async with self.session.post(
|
702
|
+
f"{server_url}/cmd", headers=readonly_headers, json=readonly_data
|
703
|
+
) as response:
|
637
704
|
duration = time.time() - start_time
|
638
705
|
# Readonly should be denied write access
|
639
706
|
if response.status == 403:
|
@@ -643,8 +710,10 @@ class SecurityTestClient:
|
|
643
710
|
auth_type=auth_type,
|
644
711
|
success=True,
|
645
712
|
status_code=response.status,
|
646
|
-
response_data={
|
647
|
-
|
713
|
+
response_data={
|
714
|
+
"message": "Correctly denied write access to readonly role"
|
715
|
+
},
|
716
|
+
duration=duration,
|
648
717
|
)
|
649
718
|
else:
|
650
719
|
return TestResult(
|
@@ -654,7 +723,7 @@ class SecurityTestClient:
|
|
654
723
|
success=False,
|
655
724
|
status_code=response.status,
|
656
725
|
error_message="Readonly role incorrectly allowed write access",
|
657
|
-
duration=duration
|
726
|
+
duration=duration,
|
658
727
|
)
|
659
728
|
except Exception as e:
|
660
729
|
duration = time.time() - start_time
|
@@ -664,9 +733,12 @@ class SecurityTestClient:
|
|
664
733
|
auth_type=auth_type,
|
665
734
|
success=False,
|
666
735
|
error_message=f"Multiple roles test error: {str(e)}",
|
667
|
-
duration=duration
|
736
|
+
duration=duration,
|
668
737
|
)
|
669
|
-
|
738
|
+
|
739
|
+
async def run_security_tests(
|
740
|
+
self, server_url: str, auth_type: str = "none", **kwargs
|
741
|
+
) -> List[TestResult]:
|
670
742
|
"""Run comprehensive security tests."""
|
671
743
|
print(f"\nš Running security tests for {server_url} ({auth_type})")
|
672
744
|
print("=" * 60)
|
@@ -675,7 +747,7 @@ class SecurityTestClient:
|
|
675
747
|
self.test_echo_command(server_url, auth_type, **kwargs),
|
676
748
|
self.test_security_command(server_url, auth_type, **kwargs),
|
677
749
|
self.test_negative_auth(server_url, auth_type, **kwargs),
|
678
|
-
self.test_role_based_access(server_url, auth_type, **kwargs)
|
750
|
+
self.test_role_based_access(server_url, auth_type, **kwargs),
|
679
751
|
]
|
680
752
|
results = []
|
681
753
|
for test in tests:
|
@@ -692,44 +764,28 @@ class SecurityTestClient:
|
|
692
764
|
print(f" Error: {result.error_message}")
|
693
765
|
print()
|
694
766
|
return results
|
767
|
+
|
695
768
|
async def test_all_scenarios(self) -> Dict[str, List[TestResult]]:
|
696
769
|
"""Test all security scenarios."""
|
697
770
|
scenarios = {
|
698
|
-
"basic_http": {
|
699
|
-
|
700
|
-
|
701
|
-
},
|
702
|
-
"
|
703
|
-
"url": "http://localhost:8001",
|
704
|
-
"auth": "api_key"
|
705
|
-
},
|
706
|
-
"https": {
|
707
|
-
"url": "https://localhost:8443",
|
708
|
-
"auth": "none"
|
709
|
-
},
|
710
|
-
"https_token": {
|
711
|
-
"url": "https://localhost:8444",
|
712
|
-
"auth": "api_key"
|
713
|
-
},
|
714
|
-
"mtls": {
|
715
|
-
"url": "https://localhost:8445",
|
716
|
-
"auth": "certificate"
|
717
|
-
}
|
771
|
+
"basic_http": {"url": "http://localhost:8000", "auth": "none"},
|
772
|
+
"http_token": {"url": "http://localhost:8001", "auth": "api_key"},
|
773
|
+
"https": {"url": "https://localhost:8443", "auth": "none"},
|
774
|
+
"https_token": {"url": "https://localhost:8444", "auth": "api_key"},
|
775
|
+
"mtls": {"url": "https://localhost:8445", "auth": "certificate"},
|
718
776
|
}
|
719
777
|
all_results = {}
|
720
778
|
for scenario_name, config in scenarios.items():
|
721
779
|
print(f"\nš Testing scenario: {scenario_name.upper()}")
|
722
780
|
print("=" * 60)
|
723
781
|
try:
|
724
|
-
results = await self.run_security_tests(
|
725
|
-
config["url"],
|
726
|
-
config["auth"]
|
727
|
-
)
|
782
|
+
results = await self.run_security_tests(config["url"], config["auth"])
|
728
783
|
all_results[scenario_name] = results
|
729
784
|
except Exception as e:
|
730
785
|
print(f"ā Failed to test {scenario_name}: {e}")
|
731
786
|
all_results[scenario_name] = []
|
732
787
|
return all_results
|
788
|
+
|
733
789
|
def print_summary(self):
|
734
790
|
"""Print test summary."""
|
735
791
|
print("\n" + "=" * 80)
|
@@ -753,16 +809,27 @@ class SecurityTestClient:
|
|
753
809
|
for result in self.test_results:
|
754
810
|
if result.success:
|
755
811
|
print(f" - {result.test_name} ({result.server_url})")
|
812
|
+
|
813
|
+
|
756
814
|
async def main():
|
757
815
|
"""Main function."""
|
758
816
|
import argparse
|
759
|
-
|
760
|
-
parser.
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
817
|
+
|
818
|
+
parser = argparse.ArgumentParser(
|
819
|
+
description="Security Test Client for MCP Proxy Adapter"
|
820
|
+
)
|
821
|
+
parser.add_argument(
|
822
|
+
"--server", default="http://localhost:8000", help="Server URL to test"
|
823
|
+
)
|
824
|
+
parser.add_argument(
|
825
|
+
"--auth",
|
826
|
+
choices=["none", "api_key", "basic", "certificate"],
|
827
|
+
default="none",
|
828
|
+
help="Authentication type",
|
829
|
+
)
|
830
|
+
parser.add_argument(
|
831
|
+
"--all-scenarios", action="store_true", help="Test all security scenarios"
|
832
|
+
)
|
766
833
|
parser.add_argument("--token", help="API token for authentication")
|
767
834
|
parser.add_argument("--cert", help="Client certificate file")
|
768
835
|
parser.add_argument("--key", help="Client private key file")
|
@@ -778,5 +845,7 @@ async def main():
|
|
778
845
|
async with SecurityTestClient(args.server) as client:
|
779
846
|
await client.run_security_tests(args.server, args.auth, token=args.token)
|
780
847
|
client.print_summary()
|
848
|
+
|
849
|
+
|
781
850
|
if __name__ == "__main__":
|
782
851
|
asyncio.run(main())
|