mcp-proxy-adapter 4.1.1__py3-none-any.whl → 6.1.0__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 (253) hide show
  1. mcp_proxy_adapter/__main__.py +12 -0
  2. mcp_proxy_adapter/api/app.py +254 -33
  3. mcp_proxy_adapter/api/handlers.py +32 -6
  4. mcp_proxy_adapter/api/middleware/__init__.py +36 -30
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +243 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
  10. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  11. mcp_proxy_adapter/api/middleware/unified_security.py +152 -0
  12. mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -0
  13. mcp_proxy_adapter/commands/__init__.py +19 -4
  14. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  15. mcp_proxy_adapter/commands/base.py +66 -32
  16. mcp_proxy_adapter/commands/builtin_commands.py +95 -0
  17. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  18. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  19. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  20. mcp_proxy_adapter/commands/command_registry.py +711 -354
  21. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  22. mcp_proxy_adapter/commands/echo_command.py +81 -0
  23. mcp_proxy_adapter/commands/health_command.py +7 -0
  24. mcp_proxy_adapter/commands/help_command.py +21 -14
  25. mcp_proxy_adapter/commands/hooks.py +200 -167
  26. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  27. mcp_proxy_adapter/commands/load_command.py +176 -0
  28. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  29. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  30. mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
  31. mcp_proxy_adapter/commands/reload_command.py +48 -50
  32. mcp_proxy_adapter/commands/result.py +1 -0
  33. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  34. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  35. mcp_proxy_adapter/commands/security_command.py +488 -0
  36. mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
  37. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  38. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  39. mcp_proxy_adapter/commands/unload_command.py +158 -0
  40. mcp_proxy_adapter/config.py +159 -2
  41. mcp_proxy_adapter/core/app_factory.py +326 -0
  42. mcp_proxy_adapter/core/auth_validator.py +606 -0
  43. mcp_proxy_adapter/core/certificate_utils.py +827 -0
  44. mcp_proxy_adapter/core/client_security.py +384 -0
  45. mcp_proxy_adapter/core/config_converter.py +405 -0
  46. mcp_proxy_adapter/core/config_validator.py +218 -0
  47. mcp_proxy_adapter/core/logging.py +19 -3
  48. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  49. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  50. mcp_proxy_adapter/core/protocol_manager.py +235 -0
  51. mcp_proxy_adapter/core/proxy_client.py +602 -0
  52. mcp_proxy_adapter/core/proxy_registration.py +522 -0
  53. mcp_proxy_adapter/core/role_utils.py +426 -0
  54. mcp_proxy_adapter/core/security_adapter.py +370 -0
  55. mcp_proxy_adapter/core/security_factory.py +239 -0
  56. mcp_proxy_adapter/core/security_integration.py +277 -0
  57. mcp_proxy_adapter/core/server_adapter.py +345 -0
  58. mcp_proxy_adapter/core/server_engine.py +364 -0
  59. mcp_proxy_adapter/core/settings.py +1 -0
  60. mcp_proxy_adapter/core/ssl_utils.py +233 -0
  61. mcp_proxy_adapter/core/transport_manager.py +292 -0
  62. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  63. mcp_proxy_adapter/custom_openapi.py +22 -11
  64. mcp_proxy_adapter/examples/README.md +230 -97
  65. mcp_proxy_adapter/examples/README_EN.md +258 -0
  66. mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
  67. mcp_proxy_adapter/examples/__pycache__/security_configurations.cpython-312.pyc +0 -0
  68. mcp_proxy_adapter/examples/__pycache__/security_test_client.cpython-312.pyc +0 -0
  69. mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
  70. mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
  71. mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +39 -0
  72. mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +25 -0
  73. mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
  74. mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
  75. mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
  76. mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
  77. mcp_proxy_adapter/examples/cert_config.json +9 -0
  78. mcp_proxy_adapter/examples/certs/admin.crt +32 -0
  79. mcp_proxy_adapter/examples/certs/admin.key +52 -0
  80. mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
  81. mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
  82. mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
  83. mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
  84. mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
  85. mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
  86. mcp_proxy_adapter/examples/certs/client.crt +32 -0
  87. mcp_proxy_adapter/examples/certs/client.key +52 -0
  88. mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
  89. mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
  90. mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
  91. mcp_proxy_adapter/examples/certs/client_user.key +52 -0
  92. mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
  93. mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
  94. mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
  95. mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
  96. mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
  97. mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
  98. mcp_proxy_adapter/examples/certs/readonly.key +52 -0
  99. mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
  100. mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
  101. mcp_proxy_adapter/examples/certs/server.crt +32 -0
  102. mcp_proxy_adapter/examples/certs/server.key +52 -0
  103. mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
  104. mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
  105. mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
  106. mcp_proxy_adapter/examples/certs/user.crt +32 -0
  107. mcp_proxy_adapter/examples/certs/user.key +52 -0
  108. mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
  109. mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
  110. mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
  111. mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
  112. mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
  113. mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
  114. mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
  115. mcp_proxy_adapter/examples/commands/__init__.py +1 -0
  116. mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
  117. mcp_proxy_adapter/examples/debug_request_state.py +144 -0
  118. mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
  119. mcp_proxy_adapter/examples/demo_client.py +341 -0
  120. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
  121. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
  122. mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
  123. mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
  124. mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
  125. mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
  126. mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
  127. mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
  128. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
  129. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
  130. mcp_proxy_adapter/examples/full_application/main.py +138 -0
  131. mcp_proxy_adapter/examples/full_application/roles.json +21 -0
  132. mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
  133. mcp_proxy_adapter/examples/generate_certificates.py +121 -0
  134. mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
  135. mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
  136. mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
  137. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
  138. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
  139. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
  140. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
  141. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
  142. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
  143. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
  144. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
  145. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
  146. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
  147. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
  148. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
  149. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
  150. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
  151. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
  152. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
  153. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
  154. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
  155. mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
  156. mcp_proxy_adapter/examples/roles.json +38 -0
  157. mcp_proxy_adapter/examples/run_example.py +81 -0
  158. mcp_proxy_adapter/examples/run_security_tests.py +326 -0
  159. mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
  160. mcp_proxy_adapter/examples/security_test_client.py +743 -0
  161. mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
  162. mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
  163. mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
  164. mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
  165. mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
  166. mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
  167. mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
  168. mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
  169. mcp_proxy_adapter/examples/test_examples.py +344 -0
  170. mcp_proxy_adapter/examples/universal_client.py +628 -0
  171. mcp_proxy_adapter/main.py +186 -0
  172. mcp_proxy_adapter/utils/config_generator.py +639 -0
  173. mcp_proxy_adapter/version.py +2 -1
  174. mcp_proxy_adapter-6.1.0.dist-info/METADATA +205 -0
  175. mcp_proxy_adapter-6.1.0.dist-info/RECORD +193 -0
  176. mcp_proxy_adapter-6.1.0.dist-info/entry_points.txt +2 -0
  177. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/licenses/LICENSE +2 -2
  178. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  179. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  180. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  181. mcp_proxy_adapter/examples/__init__.py +0 -7
  182. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  183. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  184. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  185. mcp_proxy_adapter/examples/basic_server/config.json +0 -35
  186. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  187. mcp_proxy_adapter/examples/basic_server/server.py +0 -103
  188. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  189. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  190. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
  191. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  192. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  193. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  194. mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
  195. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  196. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  197. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  198. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  199. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  200. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  201. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  202. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  203. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  204. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  205. mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
  206. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  207. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  208. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  209. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  210. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  211. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  212. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  213. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  214. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  215. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  216. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  217. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  218. mcp_proxy_adapter/tests/__init__.py +0 -0
  219. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  220. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  221. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  222. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  223. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  224. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  225. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  226. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  227. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  228. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  229. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  230. mcp_proxy_adapter/tests/conftest.py +0 -131
  231. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  232. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  233. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  234. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  235. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  236. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  237. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  238. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  239. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  240. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  241. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  242. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  243. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  244. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  245. mcp_proxy_adapter/tests/test_config.py +0 -127
  246. mcp_proxy_adapter/tests/test_utils.py +0 -65
  247. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  248. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  249. mcp_proxy_adapter/tests/unit/test_config.py +0 -217
  250. mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
  251. mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
  252. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/WHEEL +0 -0
  253. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,743 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Security Test Client for MCP Proxy Adapter
4
+
5
+ This client tests various security configurations including:
6
+ - Basic HTTP
7
+ - HTTP + Token authentication
8
+ - HTTPS
9
+ - HTTPS + Token authentication
10
+ - mTLS with certificate authentication
11
+
12
+ Author: Vasiliy Zdanovskiy
13
+ email: vasilyvz@gmail.com
14
+ """
15
+
16
+ import asyncio
17
+ import json
18
+ import os
19
+ import ssl
20
+ import sys
21
+ import time
22
+ from pathlib import Path
23
+ from typing import Dict, List, Optional, Any
24
+ from dataclasses import dataclass
25
+
26
+ import aiohttp
27
+ from aiohttp import ClientSession, ClientTimeout, TCPConnector
28
+
29
+ # Add project root to path for imports
30
+ project_root = Path(__file__).parent.parent.parent
31
+ current_dir = Path(__file__).parent
32
+ parent_dir = current_dir.parent
33
+
34
+ sys.path.insert(0, str(project_root))
35
+ sys.path.insert(0, str(current_dir))
36
+ sys.path.insert(0, str(parent_dir))
37
+
38
+
39
+ @dataclass
40
+ class TestResult:
41
+ """Test result data class."""
42
+ test_name: str
43
+ server_url: str
44
+ auth_type: str
45
+ success: bool
46
+ status_code: Optional[int] = None
47
+ response_data: Optional[Dict] = None
48
+ error_message: Optional[str] = None
49
+ duration: float = 0.0
50
+
51
+
52
+ class SecurityTestClient:
53
+ """Security test client for comprehensive testing."""
54
+
55
+ def __init__(self, base_url: str = "http://localhost:8000"):
56
+ """Initialize security test client."""
57
+ self.base_url = base_url
58
+ self.session: Optional[ClientSession] = None
59
+ # Note: For basic testing, we'll use simple SSL context creation
60
+ # instead of full mcp_security_framework integration
61
+ self.security_manager = None
62
+ self.ssl_manager = None
63
+ self.auth_manager = None
64
+ self.test_results: List[TestResult] = []
65
+
66
+ # Test tokens
67
+ self.test_tokens = {
68
+ "admin": "test-token-123",
69
+ "user": "user-token-456",
70
+ "readonly": "readonly-token-123",
71
+ "guest": "guest-token-123",
72
+ "proxy": "proxy-token-123",
73
+ "invalid": "invalid-token-999"
74
+ }
75
+
76
+ # Test certificates
77
+ self.test_certificates = {
78
+ "admin": {
79
+ "cert": "mcp_proxy_adapter/examples/certs/admin_cert.pem",
80
+ "key": "mcp_proxy_adapter/examples/certs/admin_key.pem"
81
+ },
82
+ "user": {
83
+ "cert": "mcp_proxy_adapter/examples/certs/user_cert.pem",
84
+ "key": "mcp_proxy_adapter/examples/certs/user_key.pem"
85
+ },
86
+ "readonly": {
87
+ "cert": "mcp_proxy_adapter/examples/certs/readonly_cert.pem",
88
+ "key": "mcp_proxy_adapter/examples/certs/readonly_key.pem"
89
+ }
90
+ }
91
+
92
+ async def __aenter__(self):
93
+ """Async context manager entry."""
94
+ timeout = ClientTimeout(total=30)
95
+
96
+ # Create SSL context for HTTPS connections
97
+ ssl_context = self.create_ssl_context()
98
+ connector = TCPConnector(ssl=ssl_context)
99
+
100
+ self.session = ClientSession(timeout=timeout, connector=connector)
101
+ return self
102
+
103
+ def create_ssl_context_for_mtls(self) -> ssl.SSLContext:
104
+ """Create SSL context for mTLS connections."""
105
+ ssl_context = ssl.create_default_context()
106
+
107
+ # For mTLS testing
108
+ ssl_context.check_hostname = False
109
+ ssl_context.verify_mode = ssl.CERT_NONE
110
+
111
+ # Load client certificate and key
112
+ cert_file = "./certs/user_cert.pem"
113
+ key_file = "./certs/user_key.pem"
114
+ ca_cert_file = "./certs/ca_cert.pem"
115
+
116
+ if os.path.exists(cert_file) and os.path.exists(key_file):
117
+ ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
118
+
119
+ if os.path.exists(ca_cert_file):
120
+ ssl_context.load_verify_locations(cafile=ca_cert_file)
121
+
122
+ return ssl_context
123
+
124
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
125
+ """Async context manager exit."""
126
+ if self.session:
127
+ await self.session.close()
128
+
129
+ def create_ssl_context(self, cert_file: Optional[str] = None,
130
+ key_file: Optional[str] = None,
131
+ ca_cert_file: Optional[str] = None) -> ssl.SSLContext:
132
+ """Create SSL context for client."""
133
+ ssl_context = ssl.create_default_context()
134
+
135
+ # For testing with self-signed certificates
136
+ ssl_context.check_hostname = False
137
+ ssl_context.verify_mode = ssl.CERT_NONE
138
+
139
+ if cert_file and key_file:
140
+ ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
141
+
142
+ if ca_cert_file:
143
+ ssl_context.load_verify_locations(cafile=ca_cert_file)
144
+ # For testing, still don't verify
145
+ ssl_context.verify_mode = ssl.CERT_NONE
146
+
147
+ return ssl_context
148
+
149
+ def create_auth_headers(self, auth_type: str, **kwargs) -> Dict[str, str]:
150
+ """Create authentication headers."""
151
+ headers = {"Content-Type": "application/json"}
152
+
153
+ if auth_type == "api_key":
154
+ token = kwargs.get("token", "test-token-123") # Use correct token
155
+ headers["X-API-Key"] = token # Use X-API-Key header
156
+ elif auth_type == "basic":
157
+ username = kwargs.get("username", "admin")
158
+ password = kwargs.get("password", "password")
159
+ import base64
160
+ credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
161
+ headers["Authorization"] = f"Basic {credentials}"
162
+ elif auth_type == "certificate":
163
+ # For mTLS, we need to use client certificates
164
+ # This is handled by SSL context, not headers
165
+ pass
166
+
167
+ return headers
168
+
169
+ async def test_health_check(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
170
+ """Test health check endpoint."""
171
+ start_time = time.time()
172
+ test_name = f"Health Check ({auth_type})"
173
+
174
+ try:
175
+ headers = self.create_auth_headers(auth_type, **kwargs)
176
+
177
+ async with self.session.get(f"{server_url}/health", headers=headers) as response:
178
+ duration = time.time() - start_time
179
+
180
+ if response.status == 200:
181
+ data = await response.json()
182
+ return TestResult(
183
+ test_name=test_name,
184
+ server_url=server_url,
185
+ auth_type=auth_type,
186
+ success=True,
187
+ status_code=response.status,
188
+ response_data=data,
189
+ duration=duration
190
+ )
191
+ else:
192
+ error_text = await response.text()
193
+ return TestResult(
194
+ test_name=test_name,
195
+ server_url=server_url,
196
+ auth_type=auth_type,
197
+ success=False,
198
+ status_code=response.status,
199
+ error_message=f"Health check failed: {error_text}",
200
+ duration=duration
201
+ )
202
+
203
+ except Exception as e:
204
+ duration = time.time() - start_time
205
+ return TestResult(
206
+ test_name=test_name,
207
+ server_url=server_url,
208
+ auth_type=auth_type,
209
+ success=False,
210
+ error_message=f"Health check error: {str(e)}",
211
+ duration=duration
212
+ )
213
+
214
+ async def test_echo_command(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
215
+ """Test echo command."""
216
+ start_time = time.time()
217
+ test_name = f"Echo Command ({auth_type})"
218
+
219
+ try:
220
+ headers = self.create_auth_headers(auth_type, **kwargs)
221
+
222
+ data = {
223
+ "jsonrpc": "2.0",
224
+ "method": "echo",
225
+ "params": {
226
+ "message": "Hello from security test client!"
227
+ },
228
+ "id": 1
229
+ }
230
+
231
+ async with self.session.post(f"{server_url}/cmd",
232
+ headers=headers,
233
+ json=data) as response:
234
+ duration = time.time() - start_time
235
+
236
+ if response.status == 200:
237
+ data = await response.json()
238
+ return TestResult(
239
+ test_name=test_name,
240
+ server_url=server_url,
241
+ auth_type=auth_type,
242
+ success=True,
243
+ status_code=response.status,
244
+ response_data=data,
245
+ duration=duration
246
+ )
247
+ else:
248
+ error_text = await response.text()
249
+ return TestResult(
250
+ test_name=test_name,
251
+ server_url=server_url,
252
+ auth_type=auth_type,
253
+ success=False,
254
+ status_code=response.status,
255
+ error_message=f"Echo command failed: {error_text}",
256
+ duration=duration
257
+ )
258
+
259
+ except Exception as e:
260
+ duration = time.time() - start_time
261
+ return TestResult(
262
+ test_name=test_name,
263
+ server_url=server_url,
264
+ auth_type=auth_type,
265
+ success=False,
266
+ error_message=f"Echo command error: {str(e)}",
267
+ duration=duration
268
+ )
269
+
270
+ async def test_security_command(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
271
+ """Test security command."""
272
+ start_time = time.time()
273
+ test_name = f"Security Command ({auth_type})"
274
+
275
+ try:
276
+ headers = self.create_auth_headers(auth_type, **kwargs)
277
+
278
+ data = {
279
+ "jsonrpc": "2.0",
280
+ "method": "health",
281
+ "params": {},
282
+ "id": 2
283
+ }
284
+
285
+ async with self.session.post(f"{server_url}/cmd",
286
+ headers=headers,
287
+ json=data) as response:
288
+ duration = time.time() - start_time
289
+
290
+ if response.status == 200:
291
+ data = await response.json()
292
+ return TestResult(
293
+ test_name=test_name,
294
+ server_url=server_url,
295
+ auth_type=auth_type,
296
+ success=True,
297
+ status_code=response.status,
298
+ response_data=data,
299
+ duration=duration
300
+ )
301
+ else:
302
+ error_text = await response.text()
303
+ return TestResult(
304
+ test_name=test_name,
305
+ server_url=server_url,
306
+ auth_type=auth_type,
307
+ success=False,
308
+ status_code=response.status,
309
+ error_message=f"Security command failed: {error_text}",
310
+ duration=duration
311
+ )
312
+
313
+ except Exception as e:
314
+ duration = time.time() - start_time
315
+ return TestResult(
316
+ test_name=test_name,
317
+ server_url=server_url,
318
+ auth_type=auth_type,
319
+ success=False,
320
+ error_message=f"Security command error: {str(e)}",
321
+ duration=duration
322
+ )
323
+
324
+ async def test_health(self) -> TestResult:
325
+ """Test health endpoint."""
326
+ return await self.test_health_check(self.base_url, "none")
327
+
328
+ async def test_command_execution(self) -> TestResult:
329
+ """Test command execution."""
330
+ return await self.test_echo_command(self.base_url, "none")
331
+
332
+ async def test_authentication(self) -> TestResult:
333
+ """Test authentication."""
334
+ if "api_key" in self.auth_methods:
335
+ # Use first available API key
336
+ api_key = next(iter(self.api_keys.keys()), "test-token-123")
337
+ return await self.test_echo_command(self.base_url, "api_key", token=api_key)
338
+ elif "certificate" in self.auth_methods:
339
+ # For certificate auth, test with client certificate
340
+ return await self.test_echo_command(self.base_url, "certificate")
341
+ else:
342
+ return TestResult(
343
+ test_name="Authentication Test",
344
+ server_url=self.base_url,
345
+ auth_type="none",
346
+ success=False,
347
+ error_message="No authentication method available"
348
+ )
349
+
350
+ async def test_negative_authentication(self) -> TestResult:
351
+ """Test negative authentication (should fail)."""
352
+ return await self.test_echo_command(self.base_url, "api_key", token="invalid-token")
353
+
354
+ async def test_no_auth_required(self) -> TestResult:
355
+ """Test that no authentication is required."""
356
+ return await self.test_echo_command(self.base_url, "none")
357
+
358
+
359
+
360
+ async def test_negative_auth(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
361
+ """Test negative authentication scenarios."""
362
+ start_time = time.time()
363
+ test_name = f"Negative Auth ({auth_type})"
364
+
365
+ try:
366
+ # Use invalid token
367
+ headers = self.create_auth_headers("api_key", token="invalid-token-999")
368
+
369
+ data = {
370
+ "jsonrpc": "2.0",
371
+ "method": "echo",
372
+ "params": {"message": "Should fail"},
373
+ "id": 3
374
+ }
375
+
376
+ async with self.session.post(f"{server_url}/cmd",
377
+ headers=headers,
378
+ json=data) as response:
379
+ duration = time.time() - start_time
380
+
381
+ # Expected to fail with 401
382
+ if response.status == 401:
383
+ return TestResult(
384
+ test_name=test_name,
385
+ server_url=server_url,
386
+ auth_type=auth_type,
387
+ success=True, # This is expected to fail
388
+ status_code=response.status,
389
+ response_data={"expected": "authentication_failure"},
390
+ duration=duration
391
+ )
392
+ else:
393
+ return TestResult(
394
+ test_name=test_name,
395
+ server_url=server_url,
396
+ auth_type=auth_type,
397
+ success=False,
398
+ status_code=response.status,
399
+ error_message=f"Expected 401, got {response.status}",
400
+ duration=duration
401
+ )
402
+
403
+ except Exception as e:
404
+ duration = time.time() - start_time
405
+ return TestResult(
406
+ test_name=test_name,
407
+ server_url=server_url,
408
+ auth_type=auth_type,
409
+ success=False,
410
+ error_message=f"Negative auth error: {str(e)}",
411
+ duration=duration
412
+ )
413
+
414
+ async def test_role_based_access(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
415
+ """Test role-based access control."""
416
+ start_time = time.time()
417
+ test_name = f"Role-Based Access ({auth_type})"
418
+
419
+ try:
420
+ # Test with different roles
421
+ role = kwargs.get("role", "user")
422
+ token = self.test_tokens.get(role, self.test_tokens["user"])
423
+ headers = self.create_auth_headers("api_key", token=token)
424
+
425
+ data = {
426
+ "jsonrpc": "2.0",
427
+ "method": "echo",
428
+ "params": {"message": f"Testing {role} role"},
429
+ "id": 4
430
+ }
431
+
432
+ async with self.session.post(f"{server_url}/cmd",
433
+ headers=headers,
434
+ json=data) as response:
435
+ duration = time.time() - start_time
436
+
437
+ if response.status == 200:
438
+ data = await response.json()
439
+ return TestResult(
440
+ test_name=test_name,
441
+ server_url=server_url,
442
+ auth_type=auth_type,
443
+ success=True,
444
+ status_code=response.status,
445
+ response_data=data,
446
+ duration=duration
447
+ )
448
+ else:
449
+ error_text = await response.text()
450
+ return TestResult(
451
+ test_name=test_name,
452
+ server_url=server_url,
453
+ auth_type=auth_type,
454
+ success=False,
455
+ status_code=response.status,
456
+ error_message=f"Role-based access failed: {error_text}",
457
+ duration=duration
458
+ )
459
+
460
+ except Exception as e:
461
+ duration = time.time() - start_time
462
+ return TestResult(
463
+ test_name=test_name,
464
+ server_url=server_url,
465
+ auth_type=auth_type,
466
+ success=False,
467
+ error_message=f"Role-based access error: {str(e)}",
468
+ duration=duration
469
+ )
470
+
471
+ async def test_role_permissions(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
472
+ """Test role permissions with role_test command."""
473
+ start_time = time.time()
474
+ test_name = f"Role Permissions Test ({auth_type})"
475
+
476
+ try:
477
+ # Test with different roles and actions
478
+ role = kwargs.get("role", "user")
479
+ action = kwargs.get("action", "read")
480
+ token = self.test_tokens.get(role, self.test_tokens["user"])
481
+ headers = self.create_auth_headers("api_key", token=token)
482
+
483
+ data = {
484
+ "jsonrpc": "2.0",
485
+ "method": "role_test",
486
+ "params": {"action": action},
487
+ "id": 5
488
+ }
489
+
490
+ async with self.session.post(f"{server_url}/cmd",
491
+ headers=headers,
492
+ json=data) as response:
493
+ duration = time.time() - start_time
494
+
495
+ if response.status == 200:
496
+ data = await response.json()
497
+ return TestResult(
498
+ test_name=test_name,
499
+ server_url=server_url,
500
+ auth_type=auth_type,
501
+ success=True,
502
+ status_code=response.status,
503
+ response_data=data,
504
+ duration=duration
505
+ )
506
+ else:
507
+ error_text = await response.text()
508
+ return TestResult(
509
+ test_name=test_name,
510
+ server_url=server_url,
511
+ auth_type=auth_type,
512
+ success=False,
513
+ status_code=response.status,
514
+ error_message=f"Role permissions test failed: {error_text}",
515
+ duration=duration
516
+ )
517
+
518
+ except Exception as e:
519
+ duration = time.time() - start_time
520
+ return TestResult(
521
+ test_name=test_name,
522
+ server_url=server_url,
523
+ auth_type=auth_type,
524
+ success=False,
525
+ error_message=f"Role permissions test error: {str(e)}",
526
+ duration=duration
527
+ )
528
+
529
+ async def test_multiple_roles(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
530
+ """Test multiple roles with different permissions."""
531
+ start_time = time.time()
532
+ test_name = f"Multiple Roles Test ({auth_type})"
533
+
534
+ try:
535
+ # Test admin role (should have all permissions)
536
+ admin_token = self.test_tokens.get("admin", "admin-token-123")
537
+ admin_headers = self.create_auth_headers("api_key", token=admin_token)
538
+
539
+ admin_data = {
540
+ "jsonrpc": "2.0",
541
+ "method": "role_test",
542
+ "params": {"action": "manage"},
543
+ "id": 6
544
+ }
545
+
546
+ async with self.session.post(f"{server_url}/cmd",
547
+ headers=admin_headers,
548
+ json=admin_data) as response:
549
+ if response.status != 200:
550
+ return TestResult(
551
+ test_name=test_name,
552
+ server_url=server_url,
553
+ auth_type=auth_type,
554
+ success=False,
555
+ status_code=response.status,
556
+ error_message="Admin role test failed",
557
+ duration=time.time() - start_time
558
+ )
559
+
560
+ # Test readonly role (should only have read permission)
561
+ readonly_token = self.test_tokens.get("readonly", "readonly-token-123")
562
+ readonly_headers = self.create_auth_headers("api_key", token=readonly_token)
563
+
564
+ readonly_data = {
565
+ "jsonrpc": "2.0",
566
+ "method": "role_test",
567
+ "params": {"action": "write"},
568
+ }
569
+
570
+ async with self.session.post(f"{server_url}/cmd",
571
+ headers=readonly_headers,
572
+ json=readonly_data) as response:
573
+ duration = time.time() - start_time
574
+
575
+ # Readonly should be denied write access
576
+ if response.status == 403:
577
+ return TestResult(
578
+ test_name=test_name,
579
+ server_url=server_url,
580
+ auth_type=auth_type,
581
+ success=True,
582
+ status_code=response.status,
583
+ response_data={"message": "Correctly denied write access to readonly role"},
584
+ duration=duration
585
+ )
586
+ else:
587
+ return TestResult(
588
+ test_name=test_name,
589
+ server_url=server_url,
590
+ auth_type=auth_type,
591
+ success=False,
592
+ status_code=response.status,
593
+ error_message="Readonly role incorrectly allowed write access",
594
+ duration=duration
595
+ )
596
+
597
+ except Exception as e:
598
+ duration = time.time() - start_time
599
+ return TestResult(
600
+ test_name=test_name,
601
+ server_url=server_url,
602
+ auth_type=auth_type,
603
+ success=False,
604
+ error_message=f"Multiple roles test error: {str(e)}",
605
+ duration=duration
606
+ )
607
+
608
+ async def run_security_tests(self, server_url: str, auth_type: str = "none", **kwargs) -> List[TestResult]:
609
+ """Run comprehensive security tests."""
610
+ print(f"\nšŸ”’ Running security tests for {server_url} ({auth_type})")
611
+ print("=" * 60)
612
+
613
+ tests = [
614
+ self.test_health_check(server_url, auth_type, **kwargs),
615
+ self.test_echo_command(server_url, auth_type, **kwargs),
616
+ self.test_security_command(server_url, auth_type, **kwargs),
617
+ self.test_negative_auth(server_url, auth_type, **kwargs),
618
+ self.test_role_based_access(server_url, auth_type, **kwargs)
619
+ ]
620
+
621
+ results = []
622
+ for test in tests:
623
+ result = await test
624
+ results.append(result)
625
+ self.test_results.append(result)
626
+
627
+ # Print result
628
+ status = "āœ… PASS" if result.success else "āŒ FAIL"
629
+ print(f"{status} {result.test_name}")
630
+ print(f" Duration: {result.duration:.3f}s")
631
+ if result.status_code:
632
+ print(f" Status: {result.status_code}")
633
+ if result.error_message:
634
+ print(f" Error: {result.error_message}")
635
+ print()
636
+
637
+ return results
638
+
639
+ async def test_all_scenarios(self) -> Dict[str, List[TestResult]]:
640
+ """Test all security scenarios."""
641
+ scenarios = {
642
+ "basic_http": {
643
+ "url": "http://localhost:8000",
644
+ "auth": "none"
645
+ },
646
+ "http_token": {
647
+ "url": "http://localhost:8001",
648
+ "auth": "api_key"
649
+ },
650
+ "https": {
651
+ "url": "https://localhost:8443",
652
+ "auth": "none"
653
+ },
654
+ "https_token": {
655
+ "url": "https://localhost:8444",
656
+ "auth": "api_key"
657
+ },
658
+ "mtls": {
659
+ "url": "https://localhost:8445",
660
+ "auth": "certificate"
661
+ }
662
+ }
663
+
664
+ all_results = {}
665
+
666
+ for scenario_name, config in scenarios.items():
667
+ print(f"\nšŸš€ Testing scenario: {scenario_name.upper()}")
668
+ print("=" * 60)
669
+
670
+ try:
671
+ results = await self.run_security_tests(
672
+ config["url"],
673
+ config["auth"]
674
+ )
675
+ all_results[scenario_name] = results
676
+
677
+ except Exception as e:
678
+ print(f"āŒ Failed to test {scenario_name}: {e}")
679
+ all_results[scenario_name] = []
680
+
681
+ return all_results
682
+
683
+ def print_summary(self):
684
+ """Print test summary."""
685
+ print("\n" + "=" * 80)
686
+ print("šŸ“Š SECURITY TEST SUMMARY")
687
+ print("=" * 80)
688
+
689
+ total_tests = len(self.test_results)
690
+ passed_tests = sum(1 for r in self.test_results if r.success)
691
+ failed_tests = total_tests - passed_tests
692
+
693
+ print(f"Total Tests: {total_tests}")
694
+ print(f"Passed: {passed_tests} āœ…")
695
+ print(f"Failed: {failed_tests} āŒ")
696
+ print(f"Success Rate: {(passed_tests/total_tests*100):.1f}%")
697
+
698
+ if failed_tests > 0:
699
+ print("\nāŒ Failed Tests:")
700
+ for result in self.test_results:
701
+ if not result.success:
702
+ print(f" - {result.test_name} ({result.server_url})")
703
+ if result.error_message:
704
+ print(f" Error: {result.error_message}")
705
+
706
+ print("\nāœ… Passed Tests:")
707
+ for result in self.test_results:
708
+ if result.success:
709
+ print(f" - {result.test_name} ({result.server_url})")
710
+
711
+
712
+ async def main():
713
+ """Main function."""
714
+ import argparse
715
+
716
+ parser = argparse.ArgumentParser(description="Security Test Client for MCP Proxy Adapter")
717
+ parser.add_argument("--server", default="http://localhost:8000",
718
+ help="Server URL to test")
719
+ parser.add_argument("--auth", choices=["none", "api_key", "basic", "certificate"],
720
+ default="none", help="Authentication type")
721
+ parser.add_argument("--all-scenarios", action="store_true",
722
+ help="Test all security scenarios")
723
+ parser.add_argument("--token", help="API token for authentication")
724
+ parser.add_argument("--cert", help="Client certificate file")
725
+ parser.add_argument("--key", help="Client private key file")
726
+ parser.add_argument("--ca-cert", help="CA certificate file")
727
+
728
+ args = parser.parse_args()
729
+
730
+ if args.all_scenarios:
731
+ # Test all scenarios
732
+ async with SecurityTestClient() as client:
733
+ await client.test_all_scenarios()
734
+ client.print_summary()
735
+ else:
736
+ # Test single server
737
+ async with SecurityTestClient(args.server) as client:
738
+ await client.run_security_tests(args.server, args.auth, token=args.token)
739
+ client.print_summary()
740
+
741
+
742
+ if __name__ == "__main__":
743
+ asyncio.run(main())