mcp-proxy-adapter 6.9.43__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 (242) hide show
  1. mcp_proxy_adapter/__init__.py +47 -0
  2. mcp_proxy_adapter/__main__.py +13 -0
  3. mcp_proxy_adapter/api/__init__.py +0 -0
  4. mcp_proxy_adapter/api/app.py +66 -0
  5. mcp_proxy_adapter/api/core/__init__.py +18 -0
  6. mcp_proxy_adapter/api/core/app_factory.py +355 -0
  7. mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
  8. mcp_proxy_adapter/api/core/registration_context.py +356 -0
  9. mcp_proxy_adapter/api/core/registration_manager.py +266 -0
  10. mcp_proxy_adapter/api/core/registration_tasks.py +84 -0
  11. mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
  12. mcp_proxy_adapter/api/handlers.py +181 -0
  13. mcp_proxy_adapter/api/middleware/__init__.py +21 -0
  14. mcp_proxy_adapter/api/middleware/base.py +54 -0
  15. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +73 -0
  16. mcp_proxy_adapter/api/middleware/error_handling.py +76 -0
  17. mcp_proxy_adapter/api/middleware/factory.py +147 -0
  18. mcp_proxy_adapter/api/middleware/logging.py +31 -0
  19. mcp_proxy_adapter/api/middleware/performance.py +51 -0
  20. mcp_proxy_adapter/api/middleware/protocol_middleware.py +140 -0
  21. mcp_proxy_adapter/api/middleware/transport_middleware.py +87 -0
  22. mcp_proxy_adapter/api/middleware/unified_security.py +223 -0
  23. mcp_proxy_adapter/api/middleware/user_info_middleware.py +132 -0
  24. mcp_proxy_adapter/api/openapi/__init__.py +21 -0
  25. mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
  26. mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
  27. mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
  28. mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
  29. mcp_proxy_adapter/api/schemas.py +270 -0
  30. mcp_proxy_adapter/api/tool_integration.py +131 -0
  31. mcp_proxy_adapter/api/tools.py +163 -0
  32. mcp_proxy_adapter/cli/__init__.py +12 -0
  33. mcp_proxy_adapter/cli/commands/__init__.py +15 -0
  34. mcp_proxy_adapter/cli/commands/client.py +100 -0
  35. mcp_proxy_adapter/cli/commands/config_generate.py +35 -0
  36. mcp_proxy_adapter/cli/commands/config_validate.py +74 -0
  37. mcp_proxy_adapter/cli/commands/generate.py +259 -0
  38. mcp_proxy_adapter/cli/commands/server.py +174 -0
  39. mcp_proxy_adapter/cli/commands/sets.py +128 -0
  40. mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
  41. mcp_proxy_adapter/cli/examples/__init__.py +8 -0
  42. mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
  43. mcp_proxy_adapter/cli/examples/https_token.py +96 -0
  44. mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
  45. mcp_proxy_adapter/cli/main.py +63 -0
  46. mcp_proxy_adapter/cli/parser.py +338 -0
  47. mcp_proxy_adapter/cli/validators.py +231 -0
  48. mcp_proxy_adapter/client/jsonrpc_client/__init__.py +9 -0
  49. mcp_proxy_adapter/client/jsonrpc_client/client.py +42 -0
  50. mcp_proxy_adapter/client/jsonrpc_client/command_api.py +45 -0
  51. mcp_proxy_adapter/client/jsonrpc_client/proxy_api.py +224 -0
  52. mcp_proxy_adapter/client/jsonrpc_client/queue_api.py +60 -0
  53. mcp_proxy_adapter/client/jsonrpc_client/transport.py +108 -0
  54. mcp_proxy_adapter/client/proxy.py +123 -0
  55. mcp_proxy_adapter/commands/__init__.py +66 -0
  56. mcp_proxy_adapter/commands/auth_validation_command.py +69 -0
  57. mcp_proxy_adapter/commands/base.py +389 -0
  58. mcp_proxy_adapter/commands/builtin_commands.py +30 -0
  59. mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
  60. mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
  61. mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
  62. mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
  63. mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
  64. mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
  65. mcp_proxy_adapter/commands/catalog_manager.py +97 -0
  66. mcp_proxy_adapter/commands/cert_monitor_command.py +552 -0
  67. mcp_proxy_adapter/commands/certificate_management_command.py +562 -0
  68. mcp_proxy_adapter/commands/command_registry.py +298 -0
  69. mcp_proxy_adapter/commands/config_command.py +102 -0
  70. mcp_proxy_adapter/commands/dependency_container.py +40 -0
  71. mcp_proxy_adapter/commands/dependency_manager.py +143 -0
  72. mcp_proxy_adapter/commands/echo_command.py +48 -0
  73. mcp_proxy_adapter/commands/health_command.py +142 -0
  74. mcp_proxy_adapter/commands/help_command.py +175 -0
  75. mcp_proxy_adapter/commands/hooks.py +172 -0
  76. mcp_proxy_adapter/commands/key_management_command.py +484 -0
  77. mcp_proxy_adapter/commands/load_command.py +123 -0
  78. mcp_proxy_adapter/commands/plugins_command.py +246 -0
  79. mcp_proxy_adapter/commands/protocol_management_command.py +216 -0
  80. mcp_proxy_adapter/commands/proxy_registration_command.py +319 -0
  81. mcp_proxy_adapter/commands/queue_commands.py +750 -0
  82. mcp_proxy_adapter/commands/registration_status_command.py +76 -0
  83. mcp_proxy_adapter/commands/registry/__init__.py +18 -0
  84. mcp_proxy_adapter/commands/registry/command_info.py +103 -0
  85. mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
  86. mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
  87. mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
  88. mcp_proxy_adapter/commands/reload_command.py +136 -0
  89. mcp_proxy_adapter/commands/result.py +157 -0
  90. mcp_proxy_adapter/commands/role_test_command.py +99 -0
  91. mcp_proxy_adapter/commands/roles_management_command.py +502 -0
  92. mcp_proxy_adapter/commands/security_command.py +472 -0
  93. mcp_proxy_adapter/commands/settings_command.py +113 -0
  94. mcp_proxy_adapter/commands/ssl_setup_command.py +306 -0
  95. mcp_proxy_adapter/commands/token_management_command.py +500 -0
  96. mcp_proxy_adapter/commands/transport_management_command.py +129 -0
  97. mcp_proxy_adapter/commands/unload_command.py +92 -0
  98. mcp_proxy_adapter/config.py +32 -0
  99. mcp_proxy_adapter/core/__init__.py +8 -0
  100. mcp_proxy_adapter/core/app_factory.py +560 -0
  101. mcp_proxy_adapter/core/app_runner.py +318 -0
  102. mcp_proxy_adapter/core/auth_validator.py +508 -0
  103. mcp_proxy_adapter/core/certificate/__init__.py +20 -0
  104. mcp_proxy_adapter/core/certificate/certificate_creator.py +372 -0
  105. mcp_proxy_adapter/core/certificate/certificate_extractor.py +185 -0
  106. mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
  107. mcp_proxy_adapter/core/certificate/certificate_validator.py +388 -0
  108. mcp_proxy_adapter/core/certificate/ssl_context_manager.py +65 -0
  109. mcp_proxy_adapter/core/certificate_utils.py +249 -0
  110. mcp_proxy_adapter/core/client.py +608 -0
  111. mcp_proxy_adapter/core/client_manager.py +271 -0
  112. mcp_proxy_adapter/core/client_security.py +411 -0
  113. mcp_proxy_adapter/core/config/__init__.py +18 -0
  114. mcp_proxy_adapter/core/config/config.py +237 -0
  115. mcp_proxy_adapter/core/config/config_factory.py +22 -0
  116. mcp_proxy_adapter/core/config/config_loader.py +66 -0
  117. mcp_proxy_adapter/core/config/feature_manager.py +31 -0
  118. mcp_proxy_adapter/core/config/simple_config.py +116 -0
  119. mcp_proxy_adapter/core/config/simple_config_generator.py +100 -0
  120. mcp_proxy_adapter/core/config/simple_config_validator.py +380 -0
  121. mcp_proxy_adapter/core/config_converter.py +252 -0
  122. mcp_proxy_adapter/core/config_validator.py +211 -0
  123. mcp_proxy_adapter/core/crl_utils.py +362 -0
  124. mcp_proxy_adapter/core/errors.py +276 -0
  125. mcp_proxy_adapter/core/job_manager.py +54 -0
  126. mcp_proxy_adapter/core/logging.py +250 -0
  127. mcp_proxy_adapter/core/mtls_asgi.py +140 -0
  128. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  129. mcp_proxy_adapter/core/mtls_proxy.py +229 -0
  130. mcp_proxy_adapter/core/mtls_server.py +154 -0
  131. mcp_proxy_adapter/core/protocol_manager.py +232 -0
  132. mcp_proxy_adapter/core/proxy/__init__.py +19 -0
  133. mcp_proxy_adapter/core/proxy/auth_manager.py +26 -0
  134. mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +160 -0
  135. mcp_proxy_adapter/core/proxy/registration_client.py +186 -0
  136. mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
  137. mcp_proxy_adapter/core/proxy_client.py +184 -0
  138. mcp_proxy_adapter/core/proxy_registration.py +80 -0
  139. mcp_proxy_adapter/core/role_utils.py +103 -0
  140. mcp_proxy_adapter/core/security_adapter.py +343 -0
  141. mcp_proxy_adapter/core/security_factory.py +96 -0
  142. mcp_proxy_adapter/core/security_integration.py +342 -0
  143. mcp_proxy_adapter/core/server_adapter.py +251 -0
  144. mcp_proxy_adapter/core/server_engine.py +217 -0
  145. mcp_proxy_adapter/core/settings.py +260 -0
  146. mcp_proxy_adapter/core/signal_handler.py +107 -0
  147. mcp_proxy_adapter/core/ssl_utils.py +161 -0
  148. mcp_proxy_adapter/core/transport_manager.py +153 -0
  149. mcp_proxy_adapter/core/unified_config_adapter.py +471 -0
  150. mcp_proxy_adapter/core/utils.py +101 -0
  151. mcp_proxy_adapter/core/validation/__init__.py +21 -0
  152. mcp_proxy_adapter/core/validation/config_validator.py +219 -0
  153. mcp_proxy_adapter/core/validation/file_validator.py +131 -0
  154. mcp_proxy_adapter/core/validation/protocol_validator.py +190 -0
  155. mcp_proxy_adapter/core/validation/security_validator.py +140 -0
  156. mcp_proxy_adapter/core/validation/validation_result.py +27 -0
  157. mcp_proxy_adapter/custom_openapi.py +58 -0
  158. mcp_proxy_adapter/examples/__init__.py +16 -0
  159. mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
  160. mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
  161. mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
  162. mcp_proxy_adapter/examples/basic_framework/main.py +52 -0
  163. mcp_proxy_adapter/examples/bugfix_certificate_config.py +261 -0
  164. mcp_proxy_adapter/examples/cert_manager_bugfix.py +203 -0
  165. mcp_proxy_adapter/examples/check_config.py +413 -0
  166. mcp_proxy_adapter/examples/client_usage_example.py +164 -0
  167. mcp_proxy_adapter/examples/commands/__init__.py +5 -0
  168. mcp_proxy_adapter/examples/config_builder.py +234 -0
  169. mcp_proxy_adapter/examples/config_cli.py +282 -0
  170. mcp_proxy_adapter/examples/create_test_configs.py +174 -0
  171. mcp_proxy_adapter/examples/debug_request_state.py +130 -0
  172. mcp_proxy_adapter/examples/debug_role_chain.py +191 -0
  173. mcp_proxy_adapter/examples/demo_client.py +287 -0
  174. mcp_proxy_adapter/examples/full_application/__init__.py +13 -0
  175. mcp_proxy_adapter/examples/full_application/commands/__init__.py +8 -0
  176. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +45 -0
  177. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +52 -0
  178. mcp_proxy_adapter/examples/full_application/commands/echo_command.py +32 -0
  179. mcp_proxy_adapter/examples/full_application/commands/help_command.py +54 -0
  180. mcp_proxy_adapter/examples/full_application/commands/list_command.py +57 -0
  181. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +5 -0
  182. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +29 -0
  183. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +27 -0
  184. mcp_proxy_adapter/examples/full_application/main.py +264 -0
  185. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +81 -0
  186. mcp_proxy_adapter/examples/full_application/run_mtls.py +252 -0
  187. mcp_proxy_adapter/examples/full_application/run_simple.py +152 -0
  188. mcp_proxy_adapter/examples/full_application/test_minimal_server.py +45 -0
  189. mcp_proxy_adapter/examples/full_application/test_server.py +163 -0
  190. mcp_proxy_adapter/examples/full_application/test_simple_server.py +62 -0
  191. mcp_proxy_adapter/examples/generate_config.py +502 -0
  192. mcp_proxy_adapter/examples/proxy_registration_example.py +335 -0
  193. mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
  194. mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
  195. mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
  196. mcp_proxy_adapter/examples/queue_server_example.py +85 -0
  197. mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
  198. mcp_proxy_adapter/examples/required_certificates.py +208 -0
  199. mcp_proxy_adapter/examples/run_example.py +77 -0
  200. mcp_proxy_adapter/examples/run_full_test_suite.py +619 -0
  201. mcp_proxy_adapter/examples/run_proxy_server.py +153 -0
  202. mcp_proxy_adapter/examples/run_security_tests_fixed.py +435 -0
  203. mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
  204. mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
  205. mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
  206. mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
  207. mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
  208. mcp_proxy_adapter/examples/security_test_client.py +72 -0
  209. mcp_proxy_adapter/examples/setup/__init__.py +24 -0
  210. mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
  211. mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
  212. mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
  213. mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
  214. mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
  215. mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
  216. mcp_proxy_adapter/examples/setup_test_environment.py +235 -0
  217. mcp_proxy_adapter/examples/simple_protocol_test.py +125 -0
  218. mcp_proxy_adapter/examples/test_chk_hostname_automated.py +211 -0
  219. mcp_proxy_adapter/examples/test_config.py +205 -0
  220. mcp_proxy_adapter/examples/test_config_builder.py +110 -0
  221. mcp_proxy_adapter/examples/test_examples.py +308 -0
  222. mcp_proxy_adapter/examples/test_framework_complete.py +267 -0
  223. mcp_proxy_adapter/examples/test_mcp_server.py +187 -0
  224. mcp_proxy_adapter/examples/test_protocol_examples.py +337 -0
  225. mcp_proxy_adapter/examples/universal_client.py +674 -0
  226. mcp_proxy_adapter/examples/update_config_certificates.py +135 -0
  227. mcp_proxy_adapter/examples/validate_generator_compatibility.py +385 -0
  228. mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +61 -0
  229. mcp_proxy_adapter/integrations/__init__.py +25 -0
  230. mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
  231. mcp_proxy_adapter/main.py +313 -0
  232. mcp_proxy_adapter/openapi.py +375 -0
  233. mcp_proxy_adapter/schemas/base_schema.json +114 -0
  234. mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
  235. mcp_proxy_adapter/schemas/roles.json +37 -0
  236. mcp_proxy_adapter/schemas/roles_schema.json +162 -0
  237. mcp_proxy_adapter/version.py +5 -0
  238. mcp_proxy_adapter-6.9.43.dist-info/METADATA +739 -0
  239. mcp_proxy_adapter-6.9.43.dist-info/RECORD +242 -0
  240. mcp_proxy_adapter-6.9.43.dist-info/WHEEL +5 -0
  241. mcp_proxy_adapter-6.9.43.dist-info/entry_points.txt +12 -0
  242. mcp_proxy_adapter-6.9.43.dist-info/top_level.txt +1 -0
@@ -0,0 +1,140 @@
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.types import ASGIApp, Receive, Send, Scope
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class MTLSASGIApp:
17
+ """
18
+ Custom ASGI application that properly handles mTLS client certificates.
19
+
20
+ This wrapper ensures that client certificates are properly extracted
21
+ and made available to the FastAPI application.
22
+ """
23
+
24
+ def __init__(self, app: ASGIApp, ssl_config: Dict[str, Any]):
25
+ """
26
+ Initialize MTLS ASGI application.
27
+
28
+ Args:
29
+ app: The underlying ASGI application (FastAPI)
30
+ ssl_config: SSL configuration for mTLS
31
+ """
32
+ self.app = app
33
+ self.ssl_config = ssl_config
34
+ self.verify_client = ssl_config.get("verify_client", False)
35
+ self.client_cert_required = ssl_config.get("client_cert_required", False)
36
+
37
+ get_global_logger().info(
38
+ f"MTLS ASGI app initialized: verify_client={self.verify_client}, "
39
+ f"client_cert_required={self.client_cert_required}"
40
+ )
41
+
42
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
43
+ """
44
+ Handle ASGI request with mTLS support.
45
+
46
+ Args:
47
+ scope: ASGI scope
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
+ get_global_logger().debug(
59
+ f"Client certificate extracted: {client_cert.get('subject', {})}"
60
+ )
61
+ elif self.client_cert_required:
62
+ get_global_logger().warning("Client certificate required but not provided")
63
+ # Return 401 Unauthorized
64
+ await self._send_unauthorized_response(send)
65
+ return
66
+
67
+ # Call the underlying application
68
+ await self.app(scope, receive, send)
69
+
70
+ except Exception as e:
71
+ get_global_logger().error(f"Error in MTLS ASGI app: {e}")
72
+ await self._send_error_response(send, str(e))
73
+
74
+ def _extract_client_certificate(self, scope: Scope) -> Optional[Dict[str, Any]]:
75
+ """
76
+ Extract client certificate from SSL context.
77
+
78
+ Args:
79
+ scope: ASGI scope
80
+
81
+ Returns:
82
+ Client certificate data or None
83
+ """
84
+ try:
85
+ ssl_context = scope.get("ssl")
86
+ if not ssl_context:
87
+ return None
88
+
89
+ # Get peer certificate
90
+ cert = ssl_context.getpeercert()
91
+ if cert:
92
+ return cert
93
+
94
+ return None
95
+
96
+ except Exception as e:
97
+ get_global_logger().error(f"Failed to extract client certificate: {e}")
98
+ return None
99
+
100
+ async def _send_unauthorized_response(self, send: Send) -> None:
101
+ """
102
+ Send 401 Unauthorized response.
103
+
104
+ Args:
105
+ send: ASGI send callable
106
+ """
107
+ response = {
108
+ "type": "http.response.start",
109
+ "status": 401,
110
+ "headers": [
111
+ (b"content-type", b"application/json"),
112
+ (b"content-length", b"163"),
113
+ ],
114
+ }
115
+ await send(response)
116
+
117
+ body = b'{"jsonrpc": "2.0", "error": {"code": -32001, "message": "Unauthorized: Client certificate required"}, "id": null}'
118
+ await send({"type": "http.response.body", "body": body})
119
+
120
+ async def _send_error_response(self, send: Send, error_message: str) -> None:
121
+ """
122
+ Send error response.
123
+
124
+ Args:
125
+ send: ASGI send callable
126
+ error_message: Error message
127
+ """
128
+ response = {
129
+ "type": "http.response.start",
130
+ "status": 500,
131
+ "headers": [
132
+ (b"content-type", b"application/json"),
133
+ ],
134
+ }
135
+ await send(response)
136
+
137
+ body = f'{{"jsonrpc": "2.0", "error": {{"code": -32603, "message": "Internal error: {error_message}"}}, "id": null}}'.encode()
138
+ await send({"type": "http.response.body", "body": body})
139
+
140
+
@@ -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
+ get_global_logger().info(
41
+ f"MTLS ASGI app initialized: client_cert_required={self.client_cert_required}"
42
+ )
43
+
44
+ async def __call__(self, scope: Dict[str, Any], receive, send):
45
+ """
46
+ Handle ASGI request with mTLS support.
47
+
48
+ Args:
49
+ scope: ASGI scope dictionary
50
+ receive: ASGI receive callable
51
+ send: ASGI send callable
52
+ """
53
+ try:
54
+ # Extract client certificate from SSL context
55
+ if scope["type"] == "http" and "ssl" in scope:
56
+ client_cert = self._extract_client_certificate(scope)
57
+ if client_cert:
58
+ # Store certificate in scope for middleware access
59
+ scope["client_certificate"] = client_cert
60
+ get_global_logger().debug(
61
+ f"Client certificate extracted: {client_cert.get('subject', {})}"
62
+ )
63
+ elif self.client_cert_required:
64
+ get_global_logger().warning("Client certificate required but not provided")
65
+ # Return 401 Unauthorized
66
+ await self._send_unauthorized_response(send)
67
+ return
68
+
69
+ # Call the underlying application
70
+ await self.app(scope, receive, send)
71
+
72
+ except Exception as e:
73
+ get_global_logger().error(f"Error in MTLS ASGI app: {e}")
74
+ await self._send_error_response(send, str(e))
75
+
76
+ def _extract_client_certificate(
77
+ self, scope: Dict[str, Any]
78
+ ) -> Optional[Dict[str, Any]]:
79
+ """
80
+ Extract client certificate from SSL context.
81
+
82
+ Args:
83
+ scope: ASGI scope dictionary
84
+
85
+ Returns:
86
+ Certificate dictionary or None if not found
87
+ """
88
+ try:
89
+ ssl_context = scope.get("ssl")
90
+ if not ssl_context:
91
+ get_global_logger().debug("No SSL context found in scope")
92
+ return None
93
+
94
+ # Try to get peer certificate
95
+ if hasattr(ssl_context, "getpeercert"):
96
+ cert_data = ssl_context.getpeercert(binary_form=True)
97
+ if cert_data:
98
+ # Parse certificate
99
+ cert = x509.load_der_x509_certificate(cert_data)
100
+ return self._cert_to_dict(cert)
101
+ else:
102
+ get_global_logger().debug("No certificate data in SSL context")
103
+ return None
104
+ else:
105
+ get_global_logger().debug("SSL context has no getpeercert method")
106
+ return None
107
+
108
+ except Exception as e:
109
+ get_global_logger().error(f"Failed to extract client certificate: {e}")
110
+ return None
111
+
112
+ def _cert_to_dict(self, cert: x509.Certificate) -> Dict[str, Any]:
113
+ """
114
+ Convert x509 certificate to dictionary.
115
+
116
+ Args:
117
+ cert: x509 certificate object
118
+
119
+ Returns:
120
+ Certificate dictionary
121
+ """
122
+ try:
123
+ # Extract subject
124
+ subject = {}
125
+ for name in cert.subject:
126
+ subject[name.oid._name] = name.value
127
+
128
+ # Extract issuer
129
+ issuer = {}
130
+ for name in cert.issuer:
131
+ issuer[name.oid._name] = name.value
132
+
133
+ return {
134
+ "subject": subject,
135
+ "issuer": issuer,
136
+ "serial_number": str(cert.serial_number),
137
+ "not_valid_before": cert.not_valid_before.isoformat(),
138
+ "not_valid_after": cert.not_valid_after.isoformat(),
139
+ "version": cert.version.value,
140
+ "signature_algorithm_oid": cert.signature_algorithm_oid._name,
141
+ "public_key": {
142
+ "key_size": (
143
+ cert.public_key().key_size
144
+ if hasattr(cert.public_key(), "key_size")
145
+ else None
146
+ ),
147
+ "public_numbers": (
148
+ str(cert.public_key().public_numbers())
149
+ if hasattr(cert.public_key(), "public_numbers")
150
+ else None
151
+ ),
152
+ },
153
+ }
154
+ except Exception as e:
155
+ get_global_logger().error(f"Failed to convert certificate to dict: {e}")
156
+ return {"error": str(e)}
157
+
158
+ async def _send_unauthorized_response(self, send):
159
+ """Send 401 Unauthorized response."""
160
+ await send(
161
+ {
162
+ "type": "http.response.start",
163
+ "status": 401,
164
+ "headers": [
165
+ (b"content-type", b"application/json"),
166
+ (b"content-length", b"0"),
167
+ ],
168
+ }
169
+ )
170
+ await send({"type": "http.response.body", "body": b""})
171
+
172
+ async def _send_error_response(self, send, error_message: str):
173
+ """Send error response."""
174
+ body = f'{{"error": "{error_message}"}}'.encode("utf-8")
175
+ await send(
176
+ {
177
+ "type": "http.response.start",
178
+ "status": 500,
179
+ "headers": [
180
+ (b"content-type", b"application/json"),
181
+ (b"content-length", str(len(body)).encode()),
182
+ ],
183
+ }
184
+ )
185
+ await send({"type": "http.response.body", "body": body})
186
+
187
+
@@ -0,0 +1,229 @@
1
+ """
2
+ mTLS Proxy for MCP Proxy Adapter
3
+
4
+ This module provides mTLS proxy functionality that accepts mTLS connections
5
+ and proxies them to the internal hypercorn server.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ import asyncio
12
+ import ssl
13
+ import logging
14
+ from typing import Optional, Dict, Any
15
+
16
+ from .logging import get_global_logger
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class MTLSProxy:
22
+ """
23
+ mTLS Proxy that accepts mTLS connections and proxies them to internal server.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ external_host: str,
29
+ external_port: int,
30
+ internal_host: str = "127.0.0.1",
31
+ internal_port: int = 9000,
32
+ cert_file: Optional[str] = None,
33
+ key_file: Optional[str] = None,
34
+ ca_cert: Optional[str] = None,
35
+ ):
36
+ """
37
+ Initialize mTLS Proxy.
38
+
39
+ Args:
40
+ external_host: External host to bind to
41
+ external_port: External port to bind to
42
+ internal_host: Internal server host
43
+ internal_port: Internal server port
44
+ cert_file: Server certificate file
45
+ key_file: Server private key file
46
+ ca_cert: CA certificate file for client verification
47
+ """
48
+ self.external_host = external_host
49
+ self.external_port = external_port
50
+ self.internal_host = internal_host
51
+ self.internal_port = internal_port
52
+ self.cert_file = cert_file
53
+ self.key_file = key_file
54
+ self.ca_cert = ca_cert
55
+ self.server = None
56
+
57
+ async def start(self):
58
+ """Start the mTLS proxy server."""
59
+ try:
60
+ # Create SSL context
61
+ ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
62
+ ssl_context.load_cert_chain(self.cert_file, self.key_file)
63
+
64
+ if self.ca_cert:
65
+ ssl_context.load_verify_locations(self.ca_cert)
66
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
67
+ else:
68
+ ssl_context.verify_mode = ssl.CERT_NONE
69
+
70
+ # Start server
71
+ self.server = await asyncio.start_server(
72
+ self._handle_client,
73
+ self.external_host,
74
+ self.external_port,
75
+ ssl=ssl_context,
76
+ )
77
+
78
+ get_global_logger().info(
79
+ f"🔐 mTLS Proxy started on {self.external_host}:{self.external_port}"
80
+ )
81
+ get_global_logger().info(
82
+ f"🌐 Proxying to {self.internal_host}:{self.internal_port}"
83
+ )
84
+
85
+ except Exception as e:
86
+ get_global_logger().error(f"❌ Failed to start mTLS proxy: {e}")
87
+ raise
88
+
89
+ async def _proxy_data(self, reader, writer, direction):
90
+ """Proxy data between reader and writer."""
91
+ try:
92
+ while True:
93
+ data = await reader.read(4096)
94
+ if not data:
95
+ break
96
+ writer.write(data)
97
+ await writer.drain()
98
+ except Exception as e:
99
+ get_global_logger().debug(f"Proxy connection closed ({direction}): {e}")
100
+ finally:
101
+ try:
102
+ writer.close()
103
+ await writer.wait_closed()
104
+ except Exception:
105
+ pass
106
+
107
+ async def _handle_client(self, reader, writer):
108
+ """
109
+ Handle incoming client connection.
110
+
111
+ Args:
112
+ reader: Client reader stream
113
+ writer: Client writer stream
114
+ """
115
+ internal_reader = None
116
+ internal_writer = None
117
+
118
+ try:
119
+ # Connect to internal server
120
+ internal_reader, internal_writer = await asyncio.open_connection(
121
+ self.internal_host, self.internal_port
122
+ )
123
+
124
+ # Create bidirectional proxy tasks
125
+ client_to_server = asyncio.create_task(
126
+ self._proxy_data(reader, internal_writer, "client->server")
127
+ )
128
+ server_to_client = asyncio.create_task(
129
+ self._proxy_data(internal_reader, writer, "server->client")
130
+ )
131
+
132
+ # Wait for either direction to complete
133
+ done, pending = await asyncio.wait(
134
+ [client_to_server, server_to_client],
135
+ return_when=asyncio.FIRST_COMPLETED,
136
+ )
137
+
138
+ # Cancel pending tasks
139
+ for task in pending:
140
+ task.cancel()
141
+
142
+ # Wait for cancellation
143
+ await asyncio.gather(*pending, return_exceptions=True)
144
+
145
+ except Exception as e:
146
+ get_global_logger().debug(f"Client connection error: {e}")
147
+ finally:
148
+ # Clean up connections
149
+ if internal_writer:
150
+ try:
151
+ internal_writer.close()
152
+ await internal_writer.wait_closed()
153
+ except Exception:
154
+ pass
155
+ if writer:
156
+ try:
157
+ writer.close()
158
+ await writer.wait_closed()
159
+ except Exception:
160
+ pass
161
+
162
+
163
+ async def start_mtls_proxy(
164
+ config: Dict[str, Any], internal_port: Optional[int] = None
165
+ ) -> Optional[MTLSProxy]:
166
+ """
167
+ Start mTLS proxy from configuration.
168
+
169
+ Args:
170
+ config: Configuration dictionary
171
+ internal_port: Internal server port (hypercorn port). If not provided,
172
+ will be calculated as external_port + 1000
173
+
174
+ Returns:
175
+ MTLSProxy instance if started successfully, None otherwise
176
+ """
177
+ try:
178
+ server_config = config.get("server", {})
179
+ transport_config = config.get("transport", {})
180
+ ssl_config = config.get("ssl", {})
181
+
182
+ # Get external host and port
183
+ external_host = server_config.get("host", "0.0.0.0")
184
+ external_port = server_config.get("port", 8001)
185
+
186
+ # Get internal port (use provided or calculate)
187
+ if internal_port is None:
188
+ internal_port = external_port + 1000
189
+
190
+ # Get certificate paths - try multiple locations
191
+ cert_file = (
192
+ ssl_config.get("cert_file")
193
+ or transport_config.get("ssl", {}).get("cert_file")
194
+ or transport_config.get("cert_file")
195
+ )
196
+ key_file = (
197
+ ssl_config.get("key_file")
198
+ or transport_config.get("ssl", {}).get("key_file")
199
+ or transport_config.get("key_file")
200
+ )
201
+ ca_cert = (
202
+ ssl_config.get("ca_cert")
203
+ or transport_config.get("ssl", {}).get("ca_cert")
204
+ or transport_config.get("ca_cert")
205
+ )
206
+
207
+ if not cert_file or not key_file:
208
+ get_global_logger().warning(
209
+ "mTLS certificates not found, skipping mTLS proxy"
210
+ )
211
+ return None
212
+
213
+ # Create and start proxy
214
+ proxy = MTLSProxy(
215
+ external_host=external_host,
216
+ external_port=external_port,
217
+ internal_host="127.0.0.1",
218
+ internal_port=internal_port,
219
+ cert_file=cert_file,
220
+ key_file=key_file,
221
+ ca_cert=ca_cert,
222
+ )
223
+
224
+ await proxy.start()
225
+ return proxy
226
+
227
+ except Exception as e:
228
+ get_global_logger().error(f"Failed to start mTLS proxy: {e}")
229
+ return None