mcp-proxy-adapter 6.3.4__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.
Files changed (129) hide show
  1. mcp_proxy_adapter/__init__.py +9 -5
  2. mcp_proxy_adapter/__main__.py +1 -1
  3. mcp_proxy_adapter/api/app.py +227 -176
  4. mcp_proxy_adapter/api/handlers.py +68 -60
  5. mcp_proxy_adapter/api/middleware/__init__.py +7 -5
  6. mcp_proxy_adapter/api/middleware/base.py +19 -16
  7. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +44 -34
  8. mcp_proxy_adapter/api/middleware/error_handling.py +57 -67
  9. mcp_proxy_adapter/api/middleware/factory.py +50 -52
  10. mcp_proxy_adapter/api/middleware/logging.py +46 -30
  11. mcp_proxy_adapter/api/middleware/performance.py +19 -16
  12. mcp_proxy_adapter/api/middleware/protocol_middleware.py +80 -50
  13. mcp_proxy_adapter/api/middleware/transport_middleware.py +26 -24
  14. mcp_proxy_adapter/api/middleware/unified_security.py +70 -51
  15. mcp_proxy_adapter/api/middleware/user_info_middleware.py +43 -34
  16. mcp_proxy_adapter/api/schemas.py +69 -43
  17. mcp_proxy_adapter/api/tool_integration.py +83 -63
  18. mcp_proxy_adapter/api/tools.py +60 -50
  19. mcp_proxy_adapter/commands/__init__.py +15 -6
  20. mcp_proxy_adapter/commands/auth_validation_command.py +107 -110
  21. mcp_proxy_adapter/commands/base.py +108 -112
  22. mcp_proxy_adapter/commands/builtin_commands.py +28 -18
  23. mcp_proxy_adapter/commands/catalog_manager.py +394 -265
  24. mcp_proxy_adapter/commands/cert_monitor_command.py +222 -204
  25. mcp_proxy_adapter/commands/certificate_management_command.py +210 -213
  26. mcp_proxy_adapter/commands/command_registry.py +275 -226
  27. mcp_proxy_adapter/commands/config_command.py +48 -33
  28. mcp_proxy_adapter/commands/dependency_container.py +22 -23
  29. mcp_proxy_adapter/commands/dependency_manager.py +65 -56
  30. mcp_proxy_adapter/commands/echo_command.py +15 -15
  31. mcp_proxy_adapter/commands/health_command.py +31 -29
  32. mcp_proxy_adapter/commands/help_command.py +97 -61
  33. mcp_proxy_adapter/commands/hooks.py +65 -49
  34. mcp_proxy_adapter/commands/key_management_command.py +148 -147
  35. mcp_proxy_adapter/commands/load_command.py +58 -40
  36. mcp_proxy_adapter/commands/plugins_command.py +80 -54
  37. mcp_proxy_adapter/commands/protocol_management_command.py +60 -48
  38. mcp_proxy_adapter/commands/proxy_registration_command.py +107 -115
  39. mcp_proxy_adapter/commands/reload_command.py +43 -37
  40. mcp_proxy_adapter/commands/result.py +26 -33
  41. mcp_proxy_adapter/commands/role_test_command.py +26 -26
  42. mcp_proxy_adapter/commands/roles_management_command.py +176 -173
  43. mcp_proxy_adapter/commands/security_command.py +134 -122
  44. mcp_proxy_adapter/commands/settings_command.py +47 -56
  45. mcp_proxy_adapter/commands/ssl_setup_command.py +109 -129
  46. mcp_proxy_adapter/commands/token_management_command.py +129 -158
  47. mcp_proxy_adapter/commands/transport_management_command.py +41 -36
  48. mcp_proxy_adapter/commands/unload_command.py +42 -37
  49. mcp_proxy_adapter/config.py +36 -35
  50. mcp_proxy_adapter/core/__init__.py +19 -21
  51. mcp_proxy_adapter/core/app_factory.py +30 -9
  52. mcp_proxy_adapter/core/app_runner.py +81 -64
  53. mcp_proxy_adapter/core/auth_validator.py +176 -182
  54. mcp_proxy_adapter/core/certificate_utils.py +469 -426
  55. mcp_proxy_adapter/core/client.py +155 -126
  56. mcp_proxy_adapter/core/client_manager.py +60 -54
  57. mcp_proxy_adapter/core/client_security.py +108 -88
  58. mcp_proxy_adapter/core/config_converter.py +176 -143
  59. mcp_proxy_adapter/core/config_validator.py +12 -4
  60. mcp_proxy_adapter/core/crl_utils.py +21 -7
  61. mcp_proxy_adapter/core/errors.py +64 -20
  62. mcp_proxy_adapter/core/logging.py +34 -29
  63. mcp_proxy_adapter/core/mtls_asgi.py +29 -25
  64. mcp_proxy_adapter/core/mtls_asgi_app.py +66 -54
  65. mcp_proxy_adapter/core/protocol_manager.py +154 -104
  66. mcp_proxy_adapter/core/proxy_client.py +202 -144
  67. mcp_proxy_adapter/core/proxy_registration.py +7 -3
  68. mcp_proxy_adapter/core/role_utils.py +139 -125
  69. mcp_proxy_adapter/core/security_adapter.py +88 -77
  70. mcp_proxy_adapter/core/security_factory.py +50 -44
  71. mcp_proxy_adapter/core/security_integration.py +72 -24
  72. mcp_proxy_adapter/core/server_adapter.py +68 -64
  73. mcp_proxy_adapter/core/server_engine.py +71 -53
  74. mcp_proxy_adapter/core/settings.py +68 -58
  75. mcp_proxy_adapter/core/ssl_utils.py +69 -56
  76. mcp_proxy_adapter/core/transport_manager.py +72 -60
  77. mcp_proxy_adapter/core/unified_config_adapter.py +201 -150
  78. mcp_proxy_adapter/core/utils.py +4 -2
  79. mcp_proxy_adapter/custom_openapi.py +107 -99
  80. mcp_proxy_adapter/examples/basic_framework/main.py +9 -2
  81. mcp_proxy_adapter/examples/commands/__init__.py +1 -1
  82. mcp_proxy_adapter/examples/create_certificates_simple.py +182 -71
  83. mcp_proxy_adapter/examples/debug_request_state.py +38 -19
  84. mcp_proxy_adapter/examples/debug_role_chain.py +53 -20
  85. mcp_proxy_adapter/examples/demo_client.py +48 -36
  86. mcp_proxy_adapter/examples/examples/basic_framework/main.py +9 -2
  87. mcp_proxy_adapter/examples/examples/full_application/__init__.py +1 -0
  88. mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +22 -10
  89. mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +24 -17
  90. mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +16 -3
  91. mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +13 -3
  92. mcp_proxy_adapter/examples/examples/full_application/main.py +27 -2
  93. mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +48 -14
  94. mcp_proxy_adapter/examples/full_application/__init__.py +1 -0
  95. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +22 -10
  96. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +24 -17
  97. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +16 -3
  98. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +13 -3
  99. mcp_proxy_adapter/examples/full_application/main.py +27 -2
  100. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +48 -14
  101. mcp_proxy_adapter/examples/generate_all_certificates.py +198 -73
  102. mcp_proxy_adapter/examples/generate_certificates.py +31 -16
  103. mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +220 -74
  104. mcp_proxy_adapter/examples/generate_test_configs.py +68 -91
  105. mcp_proxy_adapter/examples/proxy_registration_example.py +76 -75
  106. mcp_proxy_adapter/examples/run_example.py +23 -5
  107. mcp_proxy_adapter/examples/run_full_test_suite.py +109 -71
  108. mcp_proxy_adapter/examples/run_proxy_server.py +22 -9
  109. mcp_proxy_adapter/examples/run_security_tests.py +103 -41
  110. mcp_proxy_adapter/examples/run_security_tests_fixed.py +72 -36
  111. mcp_proxy_adapter/examples/scripts/config_generator.py +288 -187
  112. mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +185 -72
  113. mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +220 -74
  114. mcp_proxy_adapter/examples/security_test_client.py +196 -127
  115. mcp_proxy_adapter/examples/setup_test_environment.py +17 -29
  116. mcp_proxy_adapter/examples/test_config.py +19 -4
  117. mcp_proxy_adapter/examples/test_config_generator.py +23 -7
  118. mcp_proxy_adapter/examples/test_examples.py +84 -56
  119. mcp_proxy_adapter/examples/universal_client.py +119 -62
  120. mcp_proxy_adapter/openapi.py +108 -115
  121. mcp_proxy_adapter/utils/config_generator.py +429 -274
  122. mcp_proxy_adapter/version.py +1 -2
  123. {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/METADATA +1 -1
  124. mcp_proxy_adapter-6.3.5.dist-info/RECORD +143 -0
  125. mcp_proxy_adapter-6.3.4.dist-info/RECORD +0 -143
  126. {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/WHEEL +0 -0
  127. {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/entry_points.txt +0 -0
  128. {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/licenses/LICENSE +0 -0
  129. {mcp_proxy_adapter-6.3.4.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(f"āš ļø Failed to create mTLS context with mcp_security_framework: {e}")
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
- def create_ssl_context(self, cert_file: Optional[str] = None,
168
- key_file: Optional[str] = None,
169
- ca_cert_file: Optional[str] = None) -> ssl.SSLContext:
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=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,
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(f"āš ļø Failed to create SSL context with mcp_security_framework: {e}")
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
- async def test_health_check(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
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(f"{server_url}/health", headers=headers) as response:
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
- async def test_echo_command(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
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
- "message": "Hello from security test client!"
267
- },
268
- "id": 1
302
+ "params": {"message": "Hello from security test client!"},
303
+ "id": 1,
269
304
  }
270
- async with self.session.post(f"{server_url}/cmd",
271
- headers=headers,
272
- json=data) as response:
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
- async def test_security_command(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
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
- "jsonrpc": "2.0",
314
- "method": "health",
315
- "params": {},
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(self.base_url, "api_key", token="invalid-token")
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
- async def test_negative_auth(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
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 = ssl.CERT_NONE # Don't verify server cert for testing
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(timeout=timeout, connector=connector) as temp_session:
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(f"{server_url}/cmd", json=data) as response:
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 (aiohttp.ClientError, aiohttp.ServerDisconnectedError, asyncio.TimeoutError) as e:
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={"expected": "connection_rejected", "error": str(e)},
438
- duration=duration
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(f"{server_url}/cmd",
450
- headers=headers,
451
- json=data) as response:
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
- async def test_role_based_access(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
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(f"{server_url}/cmd",
512
- headers=headers,
513
- json=data) as response:
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
- async def test_role_permissions(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
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(f"{server_url}/cmd",
564
- headers=headers,
565
- json=data) as response:
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
- async def test_multiple_roles(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
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(f"{server_url}/cmd",
614
- headers=admin_headers,
615
- json=admin_data) as response:
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("api_key", token=readonly_token)
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(f"{server_url}/cmd",
635
- headers=readonly_headers,
636
- json=readonly_data) as response:
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={"message": "Correctly denied write access to readonly role"},
647
- duration=duration
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
- async def run_security_tests(self, server_url: str, auth_type: str = "none", **kwargs) -> List[TestResult]:
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
- "url": "http://localhost:8000",
700
- "auth": "none"
701
- },
702
- "http_token": {
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
- parser = argparse.ArgumentParser(description="Security Test Client for MCP Proxy Adapter")
760
- parser.add_argument("--server", default="http://localhost:8000",
761
- help="Server URL to test")
762
- parser.add_argument("--auth", choices=["none", "api_key", "basic", "certificate"],
763
- default="none", help="Authentication type")
764
- parser.add_argument("--all-scenarios", action="store_true",
765
- help="Test all security scenarios")
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())