mcp-proxy-adapter 6.6.8__py3-none-any.whl → 6.7.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.
- mcp_proxy_adapter/api/app.py +136 -90
- 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/signal_handler.py +170 -0
- mcp_proxy_adapter/examples/config_builder.py +465 -55
- mcp_proxy_adapter/examples/generate_config.py +22 -15
- mcp_proxy_adapter/main.py +37 -1
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.6.8.dist-info → mcp_proxy_adapter-6.7.0.dist-info}/METADATA +1 -1
- {mcp_proxy_adapter-6.6.8.dist-info → mcp_proxy_adapter-6.7.0.dist-info}/RECORD +15 -12
- {mcp_proxy_adapter-6.6.8.dist-info → mcp_proxy_adapter-6.7.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.6.8.dist-info → mcp_proxy_adapter-6.7.0.dist-info}/entry_points.txt +0 -0
- {mcp_proxy_adapter-6.6.8.dist-info → mcp_proxy_adapter-6.7.0.dist-info}/top_level.txt +0 -0
mcp_proxy_adapter/api/app.py
CHANGED
@@ -41,6 +41,81 @@ from mcp_proxy_adapter.commands.command_registry import registry
|
|
41
41
|
from mcp_proxy_adapter.custom_openapi import custom_openapi_with_fallback
|
42
42
|
|
43
43
|
|
44
|
+
def _determine_registration_url(config: Dict[str, Any]) -> str:
|
45
|
+
"""
|
46
|
+
Determine the registration URL for proxy registration.
|
47
|
+
|
48
|
+
Logic:
|
49
|
+
1. Protocol: registration.protocol > server.protocol > fallback to http
|
50
|
+
2. Host: public_host > hostname (if server.host is 0.0.0.0/127.0.0.1) > server.host
|
51
|
+
3. Port: public_port > server.port
|
52
|
+
|
53
|
+
Args:
|
54
|
+
config: Application configuration
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
Complete registration URL
|
58
|
+
"""
|
59
|
+
import os
|
60
|
+
import socket
|
61
|
+
|
62
|
+
# Get server configuration
|
63
|
+
server_config = config.get("server", {})
|
64
|
+
server_host = server_config.get("host", "0.0.0.0")
|
65
|
+
server_port = server_config.get("port", 8000)
|
66
|
+
server_protocol = server_config.get("protocol", "http")
|
67
|
+
|
68
|
+
# Get registration configuration
|
69
|
+
reg_cfg = config.get("registration", config.get("proxy_registration", {}))
|
70
|
+
public_host = reg_cfg.get("public_host")
|
71
|
+
public_port = reg_cfg.get("public_port")
|
72
|
+
registration_protocol = reg_cfg.get("protocol")
|
73
|
+
|
74
|
+
# Determine protocol
|
75
|
+
if registration_protocol:
|
76
|
+
# Use protocol from registration configuration
|
77
|
+
# Convert mtls to https for URL construction (mTLS is still HTTPS)
|
78
|
+
protocol = "https" if registration_protocol == "mtls" else registration_protocol
|
79
|
+
logger.info(f"🔍 Using registration.protocol: {registration_protocol} -> {protocol}")
|
80
|
+
else:
|
81
|
+
# Fallback to server protocol
|
82
|
+
verify_client = config.get("transport", {}).get("verify_client", False)
|
83
|
+
ssl_enabled = server_protocol in ["https", "mtls"] or verify_client
|
84
|
+
protocol = "https" if ssl_enabled else "http"
|
85
|
+
logger.info(f"⚠️ Fallback to server.protocol: {server_protocol} -> {protocol} (verify_client={verify_client})")
|
86
|
+
|
87
|
+
# Determine host
|
88
|
+
if not public_host:
|
89
|
+
if server_host in ("0.0.0.0", "127.0.0.1"):
|
90
|
+
# Try to get hostname, fallback to docker host addr
|
91
|
+
try:
|
92
|
+
hostname = socket.gethostname()
|
93
|
+
# Use hostname if it's not localhost
|
94
|
+
if hostname and hostname not in ("localhost", "127.0.0.1"):
|
95
|
+
resolved_host = hostname
|
96
|
+
else:
|
97
|
+
resolved_host = os.getenv("DOCKER_HOST_ADDR", "172.17.0.1")
|
98
|
+
except Exception:
|
99
|
+
resolved_host = os.getenv("DOCKER_HOST_ADDR", "172.17.0.1")
|
100
|
+
else:
|
101
|
+
resolved_host = server_host
|
102
|
+
else:
|
103
|
+
resolved_host = public_host
|
104
|
+
|
105
|
+
# Determine port
|
106
|
+
resolved_port = public_port or server_port
|
107
|
+
|
108
|
+
# Build URL
|
109
|
+
server_url = f"{protocol}://{resolved_host}:{resolved_port}"
|
110
|
+
|
111
|
+
logger.info(
|
112
|
+
"🔍 Registration URL selection: server_host=%s, server_port=%s, public_host=%s, public_port=%s, protocol=%s, resolved_host=%s, resolved_port=%s, server_url=%s",
|
113
|
+
server_host, server_port, public_host, public_port, protocol, resolved_host, resolved_port, server_url
|
114
|
+
)
|
115
|
+
|
116
|
+
return server_url
|
117
|
+
|
118
|
+
|
44
119
|
def create_lifespan(config_path: Optional[str] = None):
|
45
120
|
"""
|
46
121
|
Create lifespan manager for the FastAPI application.
|
@@ -81,46 +156,27 @@ def create_lifespan(config_path: Optional[str] = None):
|
|
81
156
|
if not server_port:
|
82
157
|
raise ValueError("server.port is required")
|
83
158
|
|
84
|
-
|
85
|
-
|
86
|
-
public_port = reg_cfg.get("public_port")
|
87
|
-
|
88
|
-
# Check SSL configuration from new structure
|
89
|
-
# Priority: registration.protocol > server.protocol > fallback to http
|
90
|
-
reg_cfg = config.get("registration", {})
|
91
|
-
registration_protocol = reg_cfg.get("protocol")
|
92
|
-
server_protocol = config.get("server.protocol", "http")
|
93
|
-
|
94
|
-
if registration_protocol:
|
95
|
-
# Use protocol from registration configuration
|
96
|
-
# Convert mtls to https for URL construction (mTLS is still HTTPS)
|
97
|
-
protocol = "https" if registration_protocol == "mtls" else registration_protocol
|
98
|
-
logger.info(f"🔍 Using registration.protocol: {registration_protocol} -> {protocol}")
|
99
|
-
else:
|
100
|
-
# Fallback to server protocol
|
101
|
-
verify_client = config.get("transport.verify_client", False)
|
102
|
-
ssl_enabled = server_protocol in ["https", "mtls"] or verify_client
|
103
|
-
protocol = "https" if ssl_enabled else "http"
|
104
|
-
logger.info(f"⚠️ Fallback to server.protocol: {server_protocol} -> {protocol} (verify_client={verify_client})")
|
105
|
-
|
106
|
-
import os
|
107
|
-
docker_host_addr = os.getenv("DOCKER_HOST_ADDR", "172.17.0.1")
|
108
|
-
target_host = public_host or (docker_host_addr if server_host == "0.0.0.0" else server_host)
|
109
|
-
target_port = public_port or server_port
|
110
|
-
early_server_url = f"{protocol}://{target_host}:{target_port}"
|
159
|
+
# Determine registration URL using unified logic
|
160
|
+
early_server_url = _determine_registration_url(config)
|
111
161
|
try:
|
112
|
-
from mcp_proxy_adapter.core.
|
113
|
-
|
162
|
+
from mcp_proxy_adapter.core.async_proxy_registration import (
|
163
|
+
initialize_async_registration,
|
164
|
+
start_async_registration,
|
114
165
|
)
|
115
166
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
167
|
+
# Initialize async registration manager
|
168
|
+
async_manager = initialize_async_registration(config)
|
169
|
+
logger.info(
|
170
|
+
"🔍 Initialized async registration manager with server_url: %s",
|
171
|
+
early_server_url,
|
172
|
+
)
|
173
|
+
|
174
|
+
# Start async registration in background thread
|
175
|
+
start_async_registration(early_server_url)
|
176
|
+
logger.info("🚀 Started async proxy registration and heartbeat thread")
|
177
|
+
|
122
178
|
except Exception as e:
|
123
|
-
logger.error(f"Failed to
|
179
|
+
logger.error(f"Failed to initialize async registration: {e}")
|
124
180
|
|
125
181
|
# Initialize system using unified logic (may perform registration)
|
126
182
|
if config_path:
|
@@ -142,57 +198,29 @@ def create_lifespan(config_path: Optional[str] = None):
|
|
142
198
|
server_host = server_config.get("host", "0.0.0.0")
|
143
199
|
server_port = server_config.get("port", 8000)
|
144
200
|
|
145
|
-
|
146
|
-
|
147
|
-
public_port = reg_cfg.get("public_port")
|
148
|
-
|
149
|
-
# Determine protocol using the new configuration structure
|
150
|
-
# Priority: registration.protocol > server.protocol > fallback to http
|
151
|
-
reg_cfg = final_config.get("registration", final_config.get("proxy_registration", {}))
|
152
|
-
registration_protocol = reg_cfg.get("protocol")
|
153
|
-
server_protocol = final_config.get("server", {}).get("protocol", "http")
|
201
|
+
# Determine registration URL using unified logic
|
202
|
+
server_url = _determine_registration_url(final_config)
|
154
203
|
|
155
|
-
|
156
|
-
# Use protocol from registration configuration
|
157
|
-
# Convert mtls to https for URL construction (mTLS is still HTTPS)
|
158
|
-
protocol = "https" if registration_protocol == "mtls" else registration_protocol
|
159
|
-
logger.info(f"🔍 Using registration.protocol: {registration_protocol} -> {protocol}")
|
160
|
-
else:
|
161
|
-
# Fallback to server protocol
|
162
|
-
verify_client_cfg = final_config.get("transport", {}).get("verify_client", False)
|
163
|
-
ssl_enabled_final = server_protocol in ["https", "mtls"] or verify_client_cfg
|
164
|
-
protocol = "https" if ssl_enabled_final else "http"
|
165
|
-
logger.info(f"⚠️ Fallback to server.protocol: {server_protocol} -> {protocol} (verify_client={verify_client_cfg})")
|
166
|
-
|
167
|
-
import os
|
168
|
-
docker_host_addr = os.getenv("DOCKER_HOST_ADDR", "172.17.0.1")
|
169
|
-
resolved_host = public_host or (docker_host_addr if server_host == "0.0.0.0" else server_host)
|
170
|
-
resolved_port = public_port or server_port
|
171
|
-
server_url = f"{protocol}://{resolved_host}:{resolved_port}"
|
172
|
-
|
173
|
-
logger.info(
|
174
|
-
"🔍 Registration URL selection: server_host=%s, server_port=%s, public_host=%s, public_port=%s, protocol=%s",
|
175
|
-
server_host,
|
176
|
-
server_port,
|
177
|
-
public_host,
|
178
|
-
public_port,
|
179
|
-
protocol,
|
180
|
-
)
|
204
|
+
# Update async registration manager with final server URL
|
181
205
|
try:
|
182
|
-
|
183
|
-
|
184
|
-
{
|
185
|
-
"server_host": server_host,
|
186
|
-
"server_port": server_port,
|
187
|
-
"public_host": public_host,
|
188
|
-
"public_port": public_port,
|
189
|
-
"protocol": protocol,
|
190
|
-
},
|
206
|
+
from mcp_proxy_adapter.core.async_proxy_registration import (
|
207
|
+
get_async_registration_manager,
|
191
208
|
)
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
209
|
+
|
210
|
+
async_manager = get_async_registration_manager()
|
211
|
+
if async_manager:
|
212
|
+
async_manager.set_server_url(server_url)
|
213
|
+
logger.info(f"🔍 Updated async registration manager with final server_url: {server_url}")
|
214
|
+
|
215
|
+
# Get current status
|
216
|
+
status = async_manager.get_status()
|
217
|
+
logger.info(f"📊 Registration status: {status}")
|
218
|
+
else:
|
219
|
+
logger.warning("Async registration manager not available")
|
220
|
+
|
221
|
+
except Exception as e:
|
222
|
+
logger.error(f"Failed to update async registration manager: {e}")
|
223
|
+
|
196
224
|
try:
|
197
225
|
print("🔍 Registration server_url resolved to (print):", server_url)
|
198
226
|
except Exception:
|
@@ -227,12 +255,30 @@ def create_lifespan(config_path: Optional[str] = None):
|
|
227
255
|
# Shutdown events
|
228
256
|
logger.info("Application shutting down")
|
229
257
|
|
230
|
-
#
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
258
|
+
# Stop async registration manager (this will also unregister)
|
259
|
+
try:
|
260
|
+
from mcp_proxy_adapter.core.async_proxy_registration import (
|
261
|
+
stop_async_registration,
|
262
|
+
get_registration_status,
|
263
|
+
)
|
264
|
+
|
265
|
+
# Get final status before stopping
|
266
|
+
final_status = get_registration_status()
|
267
|
+
logger.info(f"📊 Final registration status: {final_status}")
|
268
|
+
|
269
|
+
# Stop async registration (this will also unregister)
|
270
|
+
stop_async_registration()
|
271
|
+
logger.info("✅ Async registration manager stopped successfully")
|
272
|
+
|
273
|
+
except Exception as e:
|
274
|
+
logger.error(f"❌ Failed to stop async registration: {e}")
|
275
|
+
|
276
|
+
# Fallback to old unregistration method
|
277
|
+
unregistration_success = await unregister_from_proxy()
|
278
|
+
if unregistration_success:
|
279
|
+
logger.info("✅ Fallback proxy unregistration completed successfully")
|
280
|
+
else:
|
281
|
+
logger.warning("⚠️ Fallback proxy unregistration failed or was disabled")
|
236
282
|
|
237
283
|
return lifespan
|
238
284
|
|
@@ -24,6 +24,9 @@ from mcp_proxy_adapter.commands.echo_command import EchoCommand
|
|
24
24
|
from mcp_proxy_adapter.commands.proxy_registration_command import (
|
25
25
|
ProxyRegistrationCommand,
|
26
26
|
)
|
27
|
+
from mcp_proxy_adapter.commands.registration_status_command import (
|
28
|
+
RegistrationStatusCommand,
|
29
|
+
)
|
27
30
|
|
28
31
|
__all__ = [
|
29
32
|
"Command",
|
@@ -43,4 +46,5 @@ __all__ = [
|
|
43
46
|
"RoleTestCommand",
|
44
47
|
"EchoCommand",
|
45
48
|
"ProxyRegistrationCommand",
|
49
|
+
"RegistrationStatusCommand",
|
46
50
|
]
|
@@ -0,0 +1,119 @@
|
|
1
|
+
"""
|
2
|
+
Registration Status Command
|
3
|
+
|
4
|
+
This command provides information about the current proxy registration status,
|
5
|
+
including async registration state, heartbeat status, and statistics.
|
6
|
+
|
7
|
+
Author: Vasiliy Zdanovskiy
|
8
|
+
email: vasilyvz@gmail.com
|
9
|
+
"""
|
10
|
+
|
11
|
+
from typing import Dict, Any
|
12
|
+
from dataclasses import dataclass
|
13
|
+
|
14
|
+
from mcp_proxy_adapter.commands.base import Command
|
15
|
+
from mcp_proxy_adapter.commands.result import SuccessResult
|
16
|
+
from mcp_proxy_adapter.core.logging import logger
|
17
|
+
|
18
|
+
|
19
|
+
@dataclass
|
20
|
+
class RegistrationStatusCommandResult(SuccessResult):
|
21
|
+
"""Result of registration status command."""
|
22
|
+
|
23
|
+
status: Dict[str, Any]
|
24
|
+
message: str = "Registration status retrieved successfully"
|
25
|
+
|
26
|
+
def to_dict(self) -> Dict[str, Any]:
|
27
|
+
"""Convert result to dictionary."""
|
28
|
+
return {
|
29
|
+
"success": True,
|
30
|
+
"status": self.status,
|
31
|
+
"message": self.message,
|
32
|
+
}
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def get_schema(cls) -> Dict[str, Any]:
|
36
|
+
"""Get JSON schema for result."""
|
37
|
+
return {
|
38
|
+
"type": "object",
|
39
|
+
"properties": {
|
40
|
+
"success": {
|
41
|
+
"type": "boolean",
|
42
|
+
"description": "Whether the command was successful",
|
43
|
+
},
|
44
|
+
"status": {
|
45
|
+
"type": "object",
|
46
|
+
"description": "Registration status information",
|
47
|
+
"properties": {
|
48
|
+
"state": {"type": "string", "description": "Current registration state"},
|
49
|
+
"server_key": {"type": "string", "description": "Server key if registered"},
|
50
|
+
"last_attempt": {"type": "number", "description": "Timestamp of last registration attempt"},
|
51
|
+
"last_success": {"type": "number", "description": "Timestamp of last successful registration"},
|
52
|
+
"last_error": {"type": "string", "description": "Last error message"},
|
53
|
+
"attempt_count": {"type": "integer", "description": "Total registration attempts"},
|
54
|
+
"success_count": {"type": "integer", "description": "Total successful registrations"},
|
55
|
+
"heartbeat_enabled": {"type": "boolean", "description": "Whether heartbeat is enabled"},
|
56
|
+
"heartbeat_interval": {"type": "integer", "description": "Heartbeat interval in seconds"},
|
57
|
+
"thread_alive": {"type": "boolean", "description": "Whether registration thread is alive"},
|
58
|
+
},
|
59
|
+
},
|
60
|
+
"message": {"type": "string", "description": "Result message"},
|
61
|
+
},
|
62
|
+
"required": ["success", "status", "message"],
|
63
|
+
}
|
64
|
+
|
65
|
+
|
66
|
+
class RegistrationStatusCommand(Command):
|
67
|
+
"""Command to get proxy registration status."""
|
68
|
+
|
69
|
+
name = "registration_status"
|
70
|
+
descr = "Get current proxy registration status and statistics"
|
71
|
+
category = "proxy"
|
72
|
+
author = "Vasiliy Zdanovskiy"
|
73
|
+
email = "vasilyvz@gmail.com"
|
74
|
+
|
75
|
+
async def execute(self, **kwargs) -> RegistrationStatusCommandResult:
|
76
|
+
"""
|
77
|
+
Execute registration status command.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
RegistrationStatusCommandResult with current status
|
81
|
+
"""
|
82
|
+
logger.info("Executing registration status command")
|
83
|
+
|
84
|
+
try:
|
85
|
+
from mcp_proxy_adapter.core.async_proxy_registration import (
|
86
|
+
get_registration_status,
|
87
|
+
)
|
88
|
+
|
89
|
+
status = get_registration_status()
|
90
|
+
|
91
|
+
logger.info(f"Registration status retrieved: {status}")
|
92
|
+
|
93
|
+
return RegistrationStatusCommandResult(
|
94
|
+
status=status,
|
95
|
+
message="Registration status retrieved successfully"
|
96
|
+
)
|
97
|
+
|
98
|
+
except Exception as e:
|
99
|
+
logger.error(f"Failed to get registration status: {e}")
|
100
|
+
|
101
|
+
error_status = {
|
102
|
+
"state": "error",
|
103
|
+
"error": str(e),
|
104
|
+
"thread_alive": False
|
105
|
+
}
|
106
|
+
|
107
|
+
return RegistrationStatusCommandResult(
|
108
|
+
status=error_status,
|
109
|
+
message=f"Failed to get registration status: {e}"
|
110
|
+
)
|
111
|
+
|
112
|
+
@classmethod
|
113
|
+
def get_schema(cls) -> Dict[str, Any]:
|
114
|
+
"""Get JSON schema for command parameters."""
|
115
|
+
return {
|
116
|
+
"type": "object",
|
117
|
+
"properties": {},
|
118
|
+
"additionalProperties": False,
|
119
|
+
}
|
@@ -16,6 +16,7 @@ from typing import Dict, Any, List, Optional
|
|
16
16
|
from fastapi import FastAPI
|
17
17
|
|
18
18
|
from mcp_proxy_adapter.core.logging import get_logger
|
19
|
+
from mcp_proxy_adapter.core.signal_handler import setup_signal_handling, is_shutdown_requested
|
19
20
|
|
20
21
|
logger = get_logger("app_runner")
|
21
22
|
|
@@ -250,6 +251,30 @@ class ApplicationRunner:
|
|
250
251
|
print(f" - {error}", file=sys.stderr)
|
251
252
|
sys.exit(1)
|
252
253
|
|
254
|
+
# Setup signal handling for graceful shutdown
|
255
|
+
def shutdown_callback():
|
256
|
+
"""Callback for graceful shutdown with proxy unregistration."""
|
257
|
+
print("\n🛑 Graceful shutdown initiated...")
|
258
|
+
try:
|
259
|
+
from mcp_proxy_adapter.core.async_proxy_registration import (
|
260
|
+
stop_async_registration,
|
261
|
+
get_registration_status,
|
262
|
+
)
|
263
|
+
|
264
|
+
# Get final status
|
265
|
+
final_status = get_registration_status()
|
266
|
+
print(f"📊 Final registration status: {final_status}")
|
267
|
+
|
268
|
+
# Stop async registration (this will unregister from proxy)
|
269
|
+
stop_async_registration()
|
270
|
+
print("✅ Proxy unregistration completed")
|
271
|
+
|
272
|
+
except Exception as e:
|
273
|
+
print(f"❌ Error during shutdown: {e}")
|
274
|
+
|
275
|
+
setup_signal_handling(shutdown_callback)
|
276
|
+
print("🔧 Signal handling configured for graceful shutdown")
|
277
|
+
|
253
278
|
# Setup hooks
|
254
279
|
self.setup_hooks()
|
255
280
|
|
@@ -276,14 +301,16 @@ class ApplicationRunner:
|
|
276
301
|
import asyncio
|
277
302
|
|
278
303
|
print(f"🚀 Starting server on {host}:{port}")
|
279
|
-
print("
|
304
|
+
print("🛑 Use Ctrl+C or send SIGTERM for graceful shutdown")
|
280
305
|
print("=" * 60)
|
281
306
|
|
282
307
|
# Run with hypercorn
|
283
308
|
asyncio.run(hypercorn.asyncio.serve(self.app, **server_kwargs))
|
284
309
|
|
285
310
|
except KeyboardInterrupt:
|
286
|
-
print("\n🛑 Server stopped by user")
|
311
|
+
print("\n🛑 Server stopped by user (Ctrl+C)")
|
312
|
+
if is_shutdown_requested():
|
313
|
+
print("✅ Graceful shutdown completed")
|
287
314
|
except Exception as e:
|
288
315
|
print(f"\n❌ Failed to start server: {e}", file=sys.stderr)
|
289
316
|
sys.exit(1)
|