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,156 @@
1
+ """
2
+ Custom ASGI application for mTLS support.
3
+
4
+ This module provides a custom ASGI application that properly handles
5
+ client certificates in mTLS connections.
6
+ """
7
+
8
+ import ssl
9
+ import logging
10
+ from typing import Dict, Any, Optional
11
+ from starlette.applications import Starlette
12
+ from starlette.requests import Request
13
+ from starlette.responses import Response
14
+ from starlette.types import ASGIApp, Receive, Send, Scope
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class MTLSASGIApp:
20
+ """
21
+ Custom ASGI application that properly handles mTLS client certificates.
22
+
23
+ This wrapper ensures that client certificates are properly extracted
24
+ and made available to the FastAPI application.
25
+ """
26
+
27
+ def __init__(self, app: ASGIApp, ssl_config: Dict[str, Any]):
28
+ """
29
+ Initialize MTLS ASGI application.
30
+
31
+ Args:
32
+ app: The underlying ASGI application (FastAPI)
33
+ ssl_config: SSL configuration for mTLS
34
+ """
35
+ self.app = app
36
+ self.ssl_config = ssl_config
37
+ self.verify_client = ssl_config.get("verify_client", False)
38
+ self.client_cert_required = ssl_config.get("client_cert_required", False)
39
+
40
+ logger.info(f"MTLS ASGI app initialized: verify_client={self.verify_client}, "
41
+ f"client_cert_required={self.client_cert_required}")
42
+
43
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
44
+ """
45
+ Handle ASGI request with mTLS support.
46
+
47
+ Args:
48
+ scope: ASGI scope
49
+ receive: ASGI receive callable
50
+ send: ASGI send callable
51
+ """
52
+ try:
53
+ # Extract client certificate from SSL context
54
+ if scope["type"] == "http" and "ssl" in scope:
55
+ client_cert = self._extract_client_certificate(scope)
56
+ if client_cert:
57
+ # Store certificate in scope for middleware access
58
+ scope["client_certificate"] = client_cert
59
+ logger.debug(f"Client certificate extracted: {client_cert.get('subject', {})}")
60
+ elif self.client_cert_required:
61
+ logger.warning("Client certificate required but not provided")
62
+ # Return 401 Unauthorized
63
+ await self._send_unauthorized_response(send)
64
+ return
65
+
66
+ # Call the underlying application
67
+ await self.app(scope, receive, send)
68
+
69
+ except Exception as e:
70
+ logger.error(f"Error in MTLS ASGI app: {e}")
71
+ await self._send_error_response(send, str(e))
72
+
73
+ def _extract_client_certificate(self, scope: Scope) -> Optional[Dict[str, Any]]:
74
+ """
75
+ Extract client certificate from SSL context.
76
+
77
+ Args:
78
+ scope: ASGI scope
79
+
80
+ Returns:
81
+ Client certificate data or None
82
+ """
83
+ try:
84
+ ssl_context = scope.get("ssl")
85
+ if not ssl_context:
86
+ return None
87
+
88
+ # Get peer certificate
89
+ cert = ssl_context.getpeercert()
90
+ if cert:
91
+ return cert
92
+
93
+ return None
94
+
95
+ except Exception as e:
96
+ logger.error(f"Failed to extract client certificate: {e}")
97
+ return None
98
+
99
+ async def _send_unauthorized_response(self, send: Send) -> None:
100
+ """
101
+ Send 401 Unauthorized response.
102
+
103
+ Args:
104
+ send: ASGI send callable
105
+ """
106
+ response = {
107
+ "type": "http.response.start",
108
+ "status": 401,
109
+ "headers": [
110
+ (b"content-type", b"application/json"),
111
+ (b"content-length", b"163"),
112
+ ],
113
+ }
114
+ await send(response)
115
+
116
+ body = b'{"jsonrpc": "2.0", "error": {"code": -32001, "message": "Unauthorized: Client certificate required"}, "id": null}'
117
+ await send({"type": "http.response.body", "body": body})
118
+
119
+ async def _send_error_response(self, send: Send, error_message: str) -> None:
120
+ """
121
+ Send error response.
122
+
123
+ Args:
124
+ send: ASGI send callable
125
+ error_message: Error message
126
+ """
127
+ response = {
128
+ "type": "http.response.start",
129
+ "status": 500,
130
+ "headers": [
131
+ (b"content-type", b"application/json"),
132
+ ],
133
+ }
134
+ await send(response)
135
+
136
+ body = f'{{"jsonrpc": "2.0", "error": {{"code": -32603, "message": "Internal error: {error_message}"}}, "id": null}}'.encode()
137
+ await send({"type": "http.response.body", "body": body})
138
+
139
+
140
+ def create_mtls_asgi_app(app: ASGIApp, ssl_config: Dict[str, Any]) -> ASGIApp:
141
+ """
142
+ Create MTLS-enabled ASGI application.
143
+
144
+ Args:
145
+ app: The underlying ASGI application (FastAPI)
146
+ ssl_config: SSL configuration for mTLS
147
+
148
+ Returns:
149
+ MTLS-enabled ASGI application
150
+ """
151
+ if ssl_config.get("mode") == "mtls" or ssl_config.get("verify_client", False):
152
+ logger.info("Creating MTLS-enabled ASGI application")
153
+ return MTLSASGIApp(app, ssl_config)
154
+ else:
155
+ logger.info("Creating standard ASGI application (no mTLS)")
156
+ return app
@@ -0,0 +1,187 @@
1
+ """
2
+ MTLS ASGI Application Wrapper
3
+
4
+ This module provides an ASGI application wrapper that extracts client certificates
5
+ from the SSL context and makes them available to FastAPI middleware.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ Version: 1.0.0
10
+ """
11
+
12
+ import logging
13
+ import ssl
14
+ from typing import Dict, Any, Optional
15
+ from cryptography import x509
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class MTLSASGIApp:
21
+ """
22
+ ASGI application wrapper for mTLS support.
23
+
24
+ Extracts client certificates from SSL context and stores them in ASGI scope
25
+ for access by FastAPI middleware.
26
+ """
27
+
28
+ def __init__(self, app, ssl_config: Dict[str, Any]):
29
+ """
30
+ Initialize MTLS ASGI app.
31
+
32
+ Args:
33
+ app: The underlying ASGI application
34
+ ssl_config: SSL configuration dictionary
35
+ """
36
+ self.app = app
37
+ self.ssl_config = ssl_config
38
+ self.client_cert_required = ssl_config.get("client_cert_required", True)
39
+
40
+ logger.info(f"MTLS ASGI app initialized: client_cert_required={self.client_cert_required}")
41
+
42
+ async def __call__(self, scope: Dict[str, Any], receive, send):
43
+ """
44
+ Handle ASGI request with mTLS support.
45
+
46
+ Args:
47
+ scope: ASGI scope dictionary
48
+ receive: ASGI receive callable
49
+ send: ASGI send callable
50
+ """
51
+ try:
52
+ # Extract client certificate from SSL context
53
+ if scope["type"] == "http" and "ssl" in scope:
54
+ client_cert = self._extract_client_certificate(scope)
55
+ if client_cert:
56
+ # Store certificate in scope for middleware access
57
+ scope["client_certificate"] = client_cert
58
+ logger.debug(f"Client certificate extracted: {client_cert.get('subject', {})}")
59
+ elif self.client_cert_required:
60
+ logger.warning("Client certificate required but not provided")
61
+ # Return 401 Unauthorized
62
+ await self._send_unauthorized_response(send)
63
+ return
64
+
65
+ # Call the underlying application
66
+ await self.app(scope, receive, send)
67
+
68
+ except Exception as e:
69
+ logger.error(f"Error in MTLS ASGI app: {e}")
70
+ await self._send_error_response(send, str(e))
71
+
72
+ def _extract_client_certificate(self, scope: Dict[str, Any]) -> Optional[Dict[str, Any]]:
73
+ """
74
+ Extract client certificate from SSL context.
75
+
76
+ Args:
77
+ scope: ASGI scope dictionary
78
+
79
+ Returns:
80
+ Certificate dictionary or None if not found
81
+ """
82
+ try:
83
+ ssl_context = scope.get("ssl")
84
+ if not ssl_context:
85
+ logger.debug("No SSL context found in scope")
86
+ return None
87
+
88
+ # Try to get peer certificate
89
+ if hasattr(ssl_context, 'getpeercert'):
90
+ cert_data = ssl_context.getpeercert(binary_form=True)
91
+ if cert_data:
92
+ # Parse certificate
93
+ cert = x509.load_der_x509_certificate(cert_data)
94
+ return self._cert_to_dict(cert)
95
+ else:
96
+ logger.debug("No certificate data in SSL context")
97
+ return None
98
+ else:
99
+ logger.debug("SSL context has no getpeercert method")
100
+ return None
101
+
102
+ except Exception as e:
103
+ logger.error(f"Failed to extract client certificate: {e}")
104
+ return None
105
+
106
+ def _cert_to_dict(self, cert: x509.Certificate) -> Dict[str, Any]:
107
+ """
108
+ Convert x509 certificate to dictionary.
109
+
110
+ Args:
111
+ cert: x509 certificate object
112
+
113
+ Returns:
114
+ Certificate dictionary
115
+ """
116
+ try:
117
+ # Extract subject
118
+ subject = {}
119
+ for name in cert.subject:
120
+ subject[name.oid._name] = name.value
121
+
122
+ # Extract issuer
123
+ issuer = {}
124
+ for name in cert.issuer:
125
+ issuer[name.oid._name] = name.value
126
+
127
+ return {
128
+ "subject": subject,
129
+ "issuer": issuer,
130
+ "serial_number": str(cert.serial_number),
131
+ "not_valid_before": cert.not_valid_before.isoformat(),
132
+ "not_valid_after": cert.not_valid_after.isoformat(),
133
+ "version": cert.version.value,
134
+ "signature_algorithm_oid": cert.signature_algorithm_oid._name,
135
+ "public_key": {
136
+ "key_size": cert.public_key().key_size if hasattr(cert.public_key(), 'key_size') else None,
137
+ "public_numbers": str(cert.public_key().public_numbers()) if hasattr(cert.public_key(), 'public_numbers') else None
138
+ }
139
+ }
140
+ except Exception as e:
141
+ logger.error(f"Failed to convert certificate to dict: {e}")
142
+ return {"error": str(e)}
143
+
144
+ async def _send_unauthorized_response(self, send):
145
+ """Send 401 Unauthorized response."""
146
+ await send({
147
+ "type": "http.response.start",
148
+ "status": 401,
149
+ "headers": [
150
+ (b"content-type", b"application/json"),
151
+ (b"content-length", b"0")
152
+ ]
153
+ })
154
+ await send({
155
+ "type": "http.response.body",
156
+ "body": b""
157
+ })
158
+
159
+ async def _send_error_response(self, send, error_message: str):
160
+ """Send error response."""
161
+ body = f'{{"error": "{error_message}"}}'.encode('utf-8')
162
+ await send({
163
+ "type": "http.response.start",
164
+ "status": 500,
165
+ "headers": [
166
+ (b"content-type", b"application/json"),
167
+ (b"content-length", str(len(body)).encode())
168
+ ]
169
+ })
170
+ await send({
171
+ "type": "http.response.body",
172
+ "body": body
173
+ })
174
+
175
+
176
+ def create_mtls_asgi_app(app, ssl_config: Dict[str, Any]):
177
+ """
178
+ Create MTLS ASGI application wrapper.
179
+
180
+ Args:
181
+ app: The underlying ASGI application
182
+ ssl_config: SSL configuration dictionary
183
+
184
+ Returns:
185
+ MTLS ASGI app wrapper
186
+ """
187
+ return MTLSASGIApp(app, ssl_config)
@@ -0,0 +1,235 @@
1
+ """
2
+ Protocol management module for MCP Proxy Adapter.
3
+
4
+ This module provides functionality for managing and validating protocol configurations,
5
+ including HTTP, HTTPS, and MTLS protocols with their respective ports.
6
+ """
7
+
8
+ import ssl
9
+ from typing import Dict, List, Optional, Tuple, Union
10
+ from urllib.parse import urlparse
11
+
12
+ from mcp_proxy_adapter.config import config
13
+ from mcp_proxy_adapter.core.logging import logger
14
+
15
+
16
+ class ProtocolManager:
17
+ """
18
+ Manages protocol configurations and validates protocol access.
19
+
20
+ This class handles the validation of allowed protocols and their associated ports,
21
+ ensuring that only configured protocols are accessible.
22
+ """
23
+
24
+ def __init__(self):
25
+ """Initialize the protocol manager."""
26
+ self._load_config()
27
+
28
+ def _load_config(self):
29
+ """Load protocol configuration from config."""
30
+ self.protocols_config = config.get("protocols", {})
31
+ self.enabled = self.protocols_config.get("enabled", True)
32
+ self.allowed_protocols = self.protocols_config.get("allowed_protocols", ["http"])
33
+ logger.debug(f"Protocol manager loaded config: enabled={self.enabled}, allowed_protocols={self.allowed_protocols}")
34
+
35
+ def reload_config(self):
36
+ """Reload protocol configuration."""
37
+ self._load_config()
38
+
39
+ def is_protocol_allowed(self, protocol: str) -> bool:
40
+ """
41
+ Check if a protocol is allowed based on configuration.
42
+
43
+ Args:
44
+ protocol: Protocol name (http, https, mtls)
45
+
46
+ Returns:
47
+ True if protocol is allowed, False otherwise
48
+ """
49
+ if not self.enabled:
50
+ logger.debug("Protocol management is disabled, allowing all protocols")
51
+ return True
52
+
53
+ protocol_lower = protocol.lower()
54
+ is_allowed = protocol_lower in self.allowed_protocols
55
+
56
+ logger.debug(f"Protocol '{protocol}' allowed: {is_allowed}")
57
+ return is_allowed
58
+
59
+ def get_protocol_port(self, protocol: str) -> Optional[int]:
60
+ """
61
+ Get the configured port for a specific protocol.
62
+
63
+ Args:
64
+ protocol: Protocol name (http, https, mtls)
65
+
66
+ Returns:
67
+ Port number if configured, None otherwise
68
+ """
69
+ protocol_lower = protocol.lower()
70
+ protocol_config = self.protocols_config.get(protocol_lower, {})
71
+
72
+ if not protocol_config.get("enabled", False):
73
+ logger.debug(f"Protocol '{protocol}' is not enabled")
74
+ return None
75
+
76
+ port = protocol_config.get("port")
77
+ logger.debug(f"Protocol '{protocol}' port: {port}")
78
+ return port
79
+
80
+ def get_allowed_protocols(self) -> List[str]:
81
+ """
82
+ Get list of all allowed protocols.
83
+
84
+ Returns:
85
+ List of allowed protocol names
86
+ """
87
+ return self.allowed_protocols.copy()
88
+
89
+ def get_protocol_config(self, protocol: str) -> Dict:
90
+ """
91
+ Get full configuration for a specific protocol.
92
+
93
+ Args:
94
+ protocol: Protocol name (http, https, mtls)
95
+
96
+ Returns:
97
+ Protocol configuration dictionary
98
+ """
99
+ protocol_lower = protocol.lower()
100
+ return self.protocols_config.get(protocol_lower, {}).copy()
101
+
102
+ def validate_url_protocol(self, url: str) -> Tuple[bool, Optional[str]]:
103
+ """
104
+ Validate if the URL protocol is allowed.
105
+
106
+ Args:
107
+ url: URL to validate
108
+
109
+ Returns:
110
+ Tuple of (is_allowed, error_message)
111
+ """
112
+ try:
113
+ parsed = urlparse(url)
114
+ protocol = parsed.scheme.lower()
115
+
116
+ if not protocol:
117
+ return False, "No protocol specified in URL"
118
+
119
+ if not self.is_protocol_allowed(protocol):
120
+ return False, f"Protocol '{protocol}' is not allowed. Allowed protocols: {self.allowed_protocols}"
121
+
122
+ return True, None
123
+
124
+ except Exception as e:
125
+ return False, f"Invalid URL format: {str(e)}"
126
+
127
+ def get_ssl_context_for_protocol(self, protocol: str) -> Optional[ssl.SSLContext]:
128
+ """
129
+ Get SSL context for HTTPS or MTLS protocol.
130
+
131
+ Args:
132
+ protocol: Protocol name (https, mtls)
133
+
134
+ Returns:
135
+ SSL context if protocol requires SSL, None otherwise
136
+ """
137
+ if protocol.lower() not in ["https", "mtls"]:
138
+ return None
139
+
140
+ ssl_config = config.get("ssl", {})
141
+
142
+ if not ssl_config.get("enabled", False):
143
+ logger.warning(f"SSL required for protocol '{protocol}' but SSL is disabled")
144
+ return None
145
+
146
+ cert_file = ssl_config.get("cert_file")
147
+ key_file = ssl_config.get("key_file")
148
+
149
+ if not cert_file or not key_file:
150
+ logger.warning(f"SSL required for protocol '{protocol}' but certificate files not configured")
151
+ return None
152
+
153
+ try:
154
+ from mcp_proxy_adapter.core.ssl_utils import SSLUtils
155
+
156
+ ssl_context = SSLUtils.create_ssl_context(
157
+ cert_file=cert_file,
158
+ key_file=key_file,
159
+ ca_cert=ssl_config.get("ca_cert"),
160
+ verify_client=protocol.lower() == "mtls" or ssl_config.get("verify_client", False),
161
+ cipher_suites=ssl_config.get("cipher_suites", []),
162
+ min_tls_version=ssl_config.get("min_tls_version", "1.2"),
163
+ max_tls_version=ssl_config.get("max_tls_version", "1.3")
164
+ )
165
+
166
+ logger.info(f"SSL context created for protocol '{protocol}'")
167
+ return ssl_context
168
+
169
+ except Exception as e:
170
+ logger.error(f"Failed to create SSL context for protocol '{protocol}': {e}")
171
+ return None
172
+
173
+ def get_protocol_info(self) -> Dict[str, Dict]:
174
+ """
175
+ Get information about all configured protocols.
176
+
177
+ Returns:
178
+ Dictionary with protocol information
179
+ """
180
+ info = {}
181
+
182
+ for protocol in ["http", "https", "mtls"]:
183
+ protocol_config = self.get_protocol_config(protocol)
184
+ info[protocol] = {
185
+ "enabled": protocol_config.get("enabled", False),
186
+ "allowed": self.is_protocol_allowed(protocol),
187
+ "port": protocol_config.get("port"),
188
+ "requires_ssl": protocol in ["https", "mtls"],
189
+ "ssl_context_available": self.get_ssl_context_for_protocol(protocol) is not None
190
+ }
191
+
192
+ return info
193
+
194
+ def validate_protocol_configuration(self) -> List[str]:
195
+ """
196
+ Validate the current protocol configuration.
197
+
198
+ Returns:
199
+ List of validation errors (empty if configuration is valid)
200
+ """
201
+ errors = []
202
+
203
+ if not self.enabled:
204
+ return errors
205
+
206
+ # Check if allowed protocols are configured
207
+ for protocol in self.allowed_protocols:
208
+ if protocol not in ["http", "https", "mtls"]:
209
+ errors.append(f"Unknown protocol '{protocol}' in allowed_protocols")
210
+ continue
211
+
212
+ protocol_config = self.get_protocol_config(protocol)
213
+
214
+ if not protocol_config.get("enabled", False):
215
+ errors.append(f"Protocol '{protocol}' is in allowed_protocols but not enabled")
216
+ continue
217
+
218
+ port = protocol_config.get("port")
219
+ if not port:
220
+ errors.append(f"Protocol '{protocol}' is enabled but no port configured")
221
+ continue
222
+
223
+ # Check SSL requirements
224
+ if protocol in ["https", "mtls"]:
225
+ ssl_config = config.get("ssl", {})
226
+ if not ssl_config.get("enabled", False):
227
+ errors.append(f"Protocol '{protocol}' requires SSL but SSL is disabled")
228
+ elif not ssl_config.get("cert_file") or not ssl_config.get("key_file"):
229
+ errors.append(f"Protocol '{protocol}' requires SSL but certificate files not configured")
230
+
231
+ return errors
232
+
233
+
234
+ # Global protocol manager instance
235
+ protocol_manager = ProtocolManager()