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.
- mcp_proxy_adapter/api/app.py +60 -15
- mcp_proxy_adapter/commands/__init__.py +4 -0
- mcp_proxy_adapter/commands/registration_status_command.py +119 -0
- mcp_proxy_adapter/core/app_runner.py +29 -2
- mcp_proxy_adapter/core/async_proxy_registration.py +285 -0
- mcp_proxy_adapter/core/mtls_proxy.py +181 -0
- mcp_proxy_adapter/core/signal_handler.py +170 -0
- mcp_proxy_adapter/examples/config_builder.py +405 -61
- mcp_proxy_adapter/examples/generate_config.py +21 -16
- mcp_proxy_adapter/main.py +86 -5
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.6.9.dist-info → mcp_proxy_adapter-6.7.1.dist-info}/METADATA +1 -1
- {mcp_proxy_adapter-6.6.9.dist-info → mcp_proxy_adapter-6.7.1.dist-info}/RECORD +16 -12
- {mcp_proxy_adapter-6.6.9.dist-info → mcp_proxy_adapter-6.7.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.6.9.dist-info → mcp_proxy_adapter-6.7.1.dist-info}/entry_points.txt +0 -0
- {mcp_proxy_adapter-6.6.9.dist-info → mcp_proxy_adapter-6.7.1.dist-info}/top_level.txt +0 -0
@@ -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()
|