mcp-proxy-adapter 6.0.0__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 (259) hide show
  1. mcp_proxy_adapter/api/app.py +174 -80
  2. mcp_proxy_adapter/api/handlers.py +16 -5
  3. mcp_proxy_adapter/api/middleware/__init__.py +7 -2
  4. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  5. mcp_proxy_adapter/api/middleware/factory.py +36 -12
  6. mcp_proxy_adapter/api/middleware/unified_security.py +152 -0
  7. mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -0
  8. mcp_proxy_adapter/commands/__init__.py +7 -1
  9. mcp_proxy_adapter/commands/base.py +7 -4
  10. mcp_proxy_adapter/commands/builtin_commands.py +8 -2
  11. mcp_proxy_adapter/commands/command_registry.py +8 -0
  12. mcp_proxy_adapter/commands/echo_command.py +81 -0
  13. mcp_proxy_adapter/commands/help_command.py +21 -14
  14. mcp_proxy_adapter/commands/proxy_registration_command.py +326 -185
  15. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  16. mcp_proxy_adapter/commands/security_command.py +488 -0
  17. mcp_proxy_adapter/commands/ssl_setup_command.py +2 -2
  18. mcp_proxy_adapter/commands/token_management_command.py +1 -1
  19. mcp_proxy_adapter/config.py +81 -21
  20. mcp_proxy_adapter/core/app_factory.py +326 -0
  21. mcp_proxy_adapter/core/client_security.py +384 -0
  22. mcp_proxy_adapter/core/logging.py +8 -3
  23. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  24. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  25. mcp_proxy_adapter/core/protocol_manager.py +9 -0
  26. mcp_proxy_adapter/core/proxy_client.py +602 -0
  27. mcp_proxy_adapter/core/proxy_registration.py +299 -47
  28. mcp_proxy_adapter/core/security_adapter.py +12 -15
  29. mcp_proxy_adapter/core/security_integration.py +277 -0
  30. mcp_proxy_adapter/core/server_adapter.py +345 -0
  31. mcp_proxy_adapter/core/server_engine.py +364 -0
  32. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  33. mcp_proxy_adapter/examples/README.md +230 -97
  34. mcp_proxy_adapter/examples/README_EN.md +258 -0
  35. mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
  36. mcp_proxy_adapter/examples/__pycache__/security_configurations.cpython-312.pyc +0 -0
  37. mcp_proxy_adapter/examples/__pycache__/security_test_client.cpython-312.pyc +0 -0
  38. mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
  39. mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
  40. mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +39 -0
  41. mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +25 -0
  42. mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
  43. mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
  44. mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
  45. mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
  46. mcp_proxy_adapter/examples/cert_config.json +9 -0
  47. mcp_proxy_adapter/examples/certs/admin.crt +32 -0
  48. mcp_proxy_adapter/examples/certs/admin.key +52 -0
  49. mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
  50. mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
  51. mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
  52. mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
  53. mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
  54. mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
  55. mcp_proxy_adapter/examples/certs/client.crt +32 -0
  56. mcp_proxy_adapter/examples/certs/client.key +52 -0
  57. mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
  58. mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
  59. mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
  60. mcp_proxy_adapter/examples/certs/client_user.key +52 -0
  61. mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
  62. mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
  63. mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
  64. mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
  65. mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
  66. mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
  67. mcp_proxy_adapter/examples/certs/readonly.key +52 -0
  68. mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
  69. mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
  70. mcp_proxy_adapter/examples/certs/server.crt +32 -0
  71. mcp_proxy_adapter/examples/certs/server.key +52 -0
  72. mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
  73. mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
  74. mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
  75. mcp_proxy_adapter/examples/certs/user.crt +32 -0
  76. mcp_proxy_adapter/examples/certs/user.key +52 -0
  77. mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
  78. mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
  79. mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
  80. mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
  81. mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
  82. mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
  83. mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
  84. mcp_proxy_adapter/examples/commands/__init__.py +1 -0
  85. mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
  86. mcp_proxy_adapter/examples/debug_request_state.py +144 -0
  87. mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
  88. mcp_proxy_adapter/examples/demo_client.py +341 -0
  89. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
  90. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
  91. mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
  92. mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
  93. mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
  94. mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
  95. mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
  96. mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
  97. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
  98. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
  99. mcp_proxy_adapter/examples/full_application/main.py +138 -0
  100. mcp_proxy_adapter/examples/full_application/roles.json +21 -0
  101. mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
  102. mcp_proxy_adapter/examples/generate_certificates.py +121 -0
  103. mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
  104. mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
  105. mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
  106. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
  107. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
  108. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
  109. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
  110. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
  111. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
  112. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
  113. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
  114. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
  115. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
  116. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
  117. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
  118. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
  119. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
  120. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
  121. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
  122. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
  123. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
  124. mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
  125. mcp_proxy_adapter/examples/roles.json +38 -0
  126. mcp_proxy_adapter/examples/run_example.py +81 -0
  127. mcp_proxy_adapter/examples/run_security_tests.py +326 -0
  128. mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
  129. mcp_proxy_adapter/examples/security_test_client.py +743 -0
  130. mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
  131. mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
  132. mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
  133. mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
  134. mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
  135. mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
  136. mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
  137. mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
  138. mcp_proxy_adapter/examples/test_examples.py +344 -0
  139. mcp_proxy_adapter/examples/universal_client.py +628 -0
  140. mcp_proxy_adapter/main.py +21 -10
  141. mcp_proxy_adapter/utils/config_generator.py +639 -0
  142. mcp_proxy_adapter/version.py +2 -1
  143. mcp_proxy_adapter-6.1.0.dist-info/METADATA +205 -0
  144. mcp_proxy_adapter-6.1.0.dist-info/RECORD +193 -0
  145. mcp_proxy_adapter-6.1.0.dist-info/entry_points.txt +2 -0
  146. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/licenses/LICENSE +2 -2
  147. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  148. mcp_proxy_adapter/api/middleware/auth_adapter.py +0 -235
  149. mcp_proxy_adapter/api/middleware/mtls_adapter.py +0 -305
  150. mcp_proxy_adapter/api/middleware/mtls_middleware.py +0 -296
  151. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  152. mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +0 -241
  153. mcp_proxy_adapter/api/middleware/roles_adapter.py +0 -365
  154. mcp_proxy_adapter/api/middleware/roles_middleware.py +0 -381
  155. mcp_proxy_adapter/api/middleware/security.py +0 -376
  156. mcp_proxy_adapter/api/middleware/token_auth_middleware.py +0 -261
  157. mcp_proxy_adapter/examples/__init__.py +0 -7
  158. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  159. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  160. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  161. mcp_proxy_adapter/examples/basic_server/config.json +0 -70
  162. mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +0 -54
  163. mcp_proxy_adapter/examples/basic_server/config_http.json +0 -70
  164. mcp_proxy_adapter/examples/basic_server/config_http_only.json +0 -52
  165. mcp_proxy_adapter/examples/basic_server/config_https.json +0 -58
  166. mcp_proxy_adapter/examples/basic_server/config_mtls.json +0 -58
  167. mcp_proxy_adapter/examples/basic_server/config_ssl.json +0 -46
  168. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  169. mcp_proxy_adapter/examples/basic_server/server.py +0 -114
  170. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  171. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  172. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -566
  173. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  174. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  175. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  176. mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +0 -105
  177. mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +0 -129
  178. mcp_proxy_adapter/examples/custom_commands/config.json +0 -118
  179. mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +0 -46
  180. mcp_proxy_adapter/examples/custom_commands/config_https_only.json +0 -46
  181. mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +0 -33
  182. mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +0 -46
  183. mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +0 -33
  184. mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +0 -33
  185. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  186. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  187. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  188. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  189. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  190. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  191. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  192. mcp_proxy_adapter/examples/custom_commands/full_help_response.json +0 -1
  193. mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +0 -629
  194. mcp_proxy_adapter/examples/custom_commands/get_openapi.py +0 -103
  195. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  196. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  197. mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +0 -129
  198. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  199. mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +0 -278
  200. mcp_proxy_adapter/examples/custom_commands/server.py +0 -252
  201. mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +0 -75
  202. mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +0 -299
  203. mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +0 -278
  204. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  205. mcp_proxy_adapter/examples/custom_commands/test_openapi.py +0 -27
  206. mcp_proxy_adapter/examples/custom_commands/test_registry.py +0 -23
  207. mcp_proxy_adapter/examples/custom_commands/test_simple.py +0 -19
  208. mcp_proxy_adapter/examples/custom_project_example/README.md +0 -103
  209. mcp_proxy_adapter/examples/custom_project_example/README_EN.md +0 -103
  210. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  211. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  212. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  213. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  214. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  215. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  216. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  217. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  218. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  219. mcp_proxy_adapter/examples/simple_custom_commands/README.md +0 -149
  220. mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +0 -149
  221. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  222. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  223. mcp_proxy_adapter/schemas/roles_schema.json +0 -162
  224. mcp_proxy_adapter/tests/__init__.py +0 -0
  225. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  226. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  227. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  228. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  229. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  230. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  231. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  232. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  233. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  234. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  235. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  236. mcp_proxy_adapter/tests/conftest.py +0 -131
  237. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  238. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  239. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  240. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  241. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  242. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  243. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  244. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  245. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  246. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  247. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  248. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  249. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  250. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  251. mcp_proxy_adapter/tests/test_config.py +0 -127
  252. mcp_proxy_adapter/tests/test_utils.py +0 -65
  253. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  254. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  255. mcp_proxy_adapter/tests/unit/test_config.py +0 -270
  256. mcp_proxy_adapter-6.0.0.dist-info/METADATA +0 -201
  257. mcp_proxy_adapter-6.0.0.dist-info/RECORD +0 -179
  258. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/WHEEL +0 -0
  259. {mcp_proxy_adapter-6.0.0.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())