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