mcp-proxy-adapter 6.6.9__py3-none-any.whl → 6.7.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.
@@ -0,0 +1,181 @@
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
+ from pathlib import Path
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class MTLSProxy:
21
+ """
22
+ mTLS Proxy that accepts mTLS connections and proxies them to internal server.
23
+ """
24
+
25
+ def __init__(self,
26
+ external_host: str,
27
+ external_port: int,
28
+ internal_host: str = "127.0.0.1",
29
+ internal_port: int = 9000,
30
+ cert_file: Optional[str] = None,
31
+ key_file: Optional[str] = None,
32
+ ca_cert: Optional[str] = None):
33
+ """
34
+ Initialize mTLS Proxy.
35
+
36
+ Args:
37
+ external_host: External host to bind to
38
+ external_port: External port to bind to
39
+ internal_host: Internal server host
40
+ internal_port: Internal server port
41
+ cert_file: Server certificate file
42
+ key_file: Server private key file
43
+ ca_cert: CA certificate file for client verification
44
+ """
45
+ self.external_host = external_host
46
+ self.external_port = external_port
47
+ self.internal_host = internal_host
48
+ self.internal_port = internal_port
49
+ self.cert_file = cert_file
50
+ self.key_file = key_file
51
+ self.ca_cert = ca_cert
52
+ self.server = None
53
+
54
+ async def start(self):
55
+ """Start the mTLS proxy server."""
56
+ try:
57
+ # Create SSL context
58
+ ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
59
+ ssl_context.load_cert_chain(self.cert_file, self.key_file)
60
+
61
+ if self.ca_cert:
62
+ ssl_context.load_verify_locations(self.ca_cert)
63
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
64
+ else:
65
+ ssl_context.verify_mode = ssl.CERT_NONE
66
+
67
+ # Start server
68
+ self.server = await asyncio.start_server(
69
+ self._handle_client,
70
+ self.external_host,
71
+ self.external_port,
72
+ ssl=ssl_context
73
+ )
74
+
75
+ logger.info(f"🔐 mTLS Proxy started on {self.external_host}:{self.external_port}")
76
+ logger.info(f"🌐 Proxying to {self.internal_host}:{self.internal_port}")
77
+
78
+ except Exception as e:
79
+ logger.error(f"❌ Failed to start mTLS proxy: {e}")
80
+ raise
81
+
82
+ async def stop(self):
83
+ """Stop the mTLS proxy server."""
84
+ if self.server:
85
+ self.server.close()
86
+ await self.server.wait_closed()
87
+ logger.info("🔐 mTLS Proxy stopped")
88
+
89
+ async def _handle_client(self, reader, writer):
90
+ """Handle client connection."""
91
+ try:
92
+ # Get client address
93
+ client_addr = writer.get_extra_info('peername')
94
+ logger.info(f"🔐 mTLS connection from {client_addr}")
95
+
96
+ # Connect to internal server
97
+ internal_reader, internal_writer = await asyncio.open_connection(
98
+ self.internal_host, self.internal_port
99
+ )
100
+
101
+ # Create bidirectional proxy
102
+ await asyncio.gather(
103
+ self._proxy_data(reader, internal_writer, "client->server"),
104
+ self._proxy_data(internal_reader, writer, "server->client")
105
+ )
106
+
107
+ except Exception as e:
108
+ logger.error(f"❌ Error handling client connection: {e}")
109
+ finally:
110
+ try:
111
+ writer.close()
112
+ await writer.wait_closed()
113
+ except:
114
+ pass
115
+
116
+ async def _proxy_data(self, reader, writer, direction):
117
+ """Proxy data between reader and writer."""
118
+ try:
119
+ while True:
120
+ data = await reader.read(4096)
121
+ if not data:
122
+ break
123
+ writer.write(data)
124
+ await writer.drain()
125
+ except Exception as e:
126
+ logger.debug(f"Proxy connection closed ({direction}): {e}")
127
+ finally:
128
+ try:
129
+ writer.close()
130
+ await writer.wait_closed()
131
+ except:
132
+ pass
133
+
134
+
135
+ async def start_mtls_proxy(config: Dict[str, Any]) -> Optional[MTLSProxy]:
136
+ """
137
+ Start mTLS proxy based on configuration.
138
+
139
+ Args:
140
+ config: Application configuration
141
+
142
+ Returns:
143
+ MTLSProxy instance if started, None otherwise
144
+ """
145
+ # Check if mTLS is enabled
146
+ protocol = config.get("server", {}).get("protocol", "http")
147
+ verify_client = config.get("transport", {}).get("verify_client", False)
148
+
149
+ # Only start mTLS proxy if mTLS is explicitly enabled
150
+ if protocol != "mtls" and not verify_client:
151
+ logger.info("🌐 Regular mode: no mTLS proxy needed")
152
+ return None
153
+
154
+ # Get configuration
155
+ server_config = config.get("server", {})
156
+ transport_config = config.get("transport", {})
157
+
158
+ external_host = server_config.get("host", "0.0.0.0")
159
+ external_port = server_config.get("port", 8000)
160
+ internal_port = external_port + 1000 # Internal port
161
+
162
+ cert_file = transport_config.get("cert_file")
163
+ key_file = transport_config.get("key_file")
164
+ ca_cert = transport_config.get("ca_cert")
165
+
166
+ if not cert_file or not key_file:
167
+ logger.warning("⚠️ mTLS enabled but certificates not configured")
168
+ return None
169
+
170
+ # Create and start proxy
171
+ proxy = MTLSProxy(
172
+ external_host=external_host,
173
+ external_port=external_port,
174
+ internal_port=internal_port,
175
+ cert_file=cert_file,
176
+ key_file=key_file,
177
+ ca_cert=ca_cert
178
+ )
179
+
180
+ await proxy.start()
181
+ return proxy
@@ -0,0 +1,170 @@
1
+ """
2
+ Signal handler for graceful shutdown with proxy unregistration.
3
+
4
+ This module provides signal handling for SIGTERM, SIGINT, and SIGHUP
5
+ to ensure proper proxy unregistration before server shutdown.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ import signal
12
+ import asyncio
13
+ import threading
14
+ from typing import Optional, Callable, Any
15
+ from mcp_proxy_adapter.core.logging import logger
16
+
17
+
18
+ class SignalHandler:
19
+ """
20
+ Signal handler for graceful shutdown with proxy unregistration.
21
+ """
22
+
23
+ def __init__(self):
24
+ """Initialize signal handler."""
25
+ self._shutdown_callback: Optional[Callable] = None
26
+ self._shutdown_event = threading.Event()
27
+ self._original_handlers = {}
28
+ self._setup_signal_handlers()
29
+
30
+ def set_shutdown_callback(self, callback: Callable):
31
+ """
32
+ Set callback function to be called during shutdown.
33
+
34
+ Args:
35
+ callback: Function to call during shutdown
36
+ """
37
+ self._shutdown_callback = callback
38
+ logger.info("Shutdown callback set for signal handler")
39
+
40
+ def _setup_signal_handlers(self):
41
+ """Setup signal handlers for graceful shutdown."""
42
+ # Handle SIGTERM (termination signal)
43
+ self._original_handlers[signal.SIGTERM] = signal.signal(
44
+ signal.SIGTERM, self._handle_shutdown_signal
45
+ )
46
+
47
+ # Handle SIGINT (Ctrl+C)
48
+ self._original_handlers[signal.SIGINT] = signal.signal(
49
+ signal.SIGINT, self._handle_shutdown_signal
50
+ )
51
+
52
+ # Handle SIGHUP (hangup signal)
53
+ self._original_handlers[signal.SIGHUP] = signal.signal(
54
+ signal.SIGHUP, self._handle_shutdown_signal
55
+ )
56
+
57
+ logger.info("Signal handlers installed for SIGTERM, SIGINT, SIGHUP")
58
+
59
+ def _handle_shutdown_signal(self, signum: int, frame: Any):
60
+ """
61
+ Handle shutdown signals.
62
+
63
+ Args:
64
+ signum: Signal number
65
+ frame: Current stack frame
66
+ """
67
+ signal_name = signal.Signals(signum).name
68
+ logger.info(f"🛑 Received {signal_name} signal, initiating graceful shutdown...")
69
+
70
+ # Set shutdown event
71
+ self._shutdown_event.set()
72
+
73
+ # Call shutdown callback if set
74
+ if self._shutdown_callback:
75
+ try:
76
+ logger.info("🔄 Executing shutdown callback...")
77
+ self._shutdown_callback()
78
+ logger.info("✅ Shutdown callback completed successfully")
79
+ except Exception as e:
80
+ logger.error(f"❌ Shutdown callback failed: {e}")
81
+
82
+ # If this is SIGINT (Ctrl+C), we might want to exit immediately after a delay
83
+ if signum == signal.SIGINT:
84
+ logger.info("⏰ SIGINT received, will exit in 5 seconds if not stopped gracefully...")
85
+ threading.Timer(5.0, self._force_exit).start()
86
+
87
+ def _force_exit(self):
88
+ """Force exit if graceful shutdown takes too long."""
89
+ if self._shutdown_event.is_set():
90
+ logger.warning("⚠️ Forcing exit after timeout")
91
+ import os
92
+ os._exit(1)
93
+
94
+ def wait_for_shutdown(self, timeout: Optional[float] = None) -> bool:
95
+ """
96
+ Wait for shutdown signal.
97
+
98
+ Args:
99
+ timeout: Maximum time to wait in seconds
100
+
101
+ Returns:
102
+ True if shutdown signal received, False if timeout
103
+ """
104
+ return self._shutdown_event.wait(timeout)
105
+
106
+ def is_shutdown_requested(self) -> bool:
107
+ """
108
+ Check if shutdown has been requested.
109
+
110
+ Returns:
111
+ True if shutdown signal received
112
+ """
113
+ return self._shutdown_event.is_set()
114
+
115
+ def restore_handlers(self):
116
+ """Restore original signal handlers."""
117
+ for sig, handler in self._original_handlers.items():
118
+ if handler is not None:
119
+ signal.signal(sig, handler)
120
+ logger.info("Original signal handlers restored")
121
+
122
+
123
+ # Global signal handler instance
124
+ _signal_handler: Optional[SignalHandler] = None
125
+
126
+
127
+ def get_signal_handler() -> SignalHandler:
128
+ """Get the global signal handler instance."""
129
+ global _signal_handler
130
+ if _signal_handler is None:
131
+ _signal_handler = SignalHandler()
132
+ return _signal_handler
133
+
134
+
135
+ def setup_signal_handling(shutdown_callback: Optional[Callable] = None):
136
+ """
137
+ Setup signal handling for graceful shutdown.
138
+
139
+ Args:
140
+ shutdown_callback: Optional callback to execute during shutdown
141
+ """
142
+ handler = get_signal_handler()
143
+ if shutdown_callback:
144
+ handler.set_shutdown_callback(shutdown_callback)
145
+ logger.info("Signal handling setup completed")
146
+
147
+
148
+ def wait_for_shutdown_signal(timeout: Optional[float] = None) -> bool:
149
+ """
150
+ Wait for shutdown signal.
151
+
152
+ Args:
153
+ timeout: Maximum time to wait in seconds
154
+
155
+ Returns:
156
+ True if shutdown signal received, False if timeout
157
+ """
158
+ handler = get_signal_handler()
159
+ return handler.wait_for_shutdown(timeout)
160
+
161
+
162
+ def is_shutdown_requested() -> bool:
163
+ """
164
+ Check if shutdown has been requested.
165
+
166
+ Returns:
167
+ True if shutdown signal received
168
+ """
169
+ handler = get_signal_handler()
170
+ return handler.is_shutdown_requested()