mcp-proxy-adapter 6.9.27__py3-none-any.whl → 6.9.29__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.
Potentially problematic release.
This version of mcp-proxy-adapter might be problematic. Click here for more details.
- mcp_proxy_adapter/__init__.py +10 -0
- mcp_proxy_adapter/__main__.py +8 -21
- mcp_proxy_adapter/api/app.py +10 -913
- mcp_proxy_adapter/api/core/__init__.py +18 -0
- mcp_proxy_adapter/api/core/app_factory.py +243 -0
- mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
- mcp_proxy_adapter/api/core/registration_manager.py +166 -0
- mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
- mcp_proxy_adapter/api/handlers.py +78 -199
- mcp_proxy_adapter/api/middleware/__init__.py +1 -44
- mcp_proxy_adapter/api/middleware/base.py +0 -42
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +0 -85
- mcp_proxy_adapter/api/middleware/error_handling.py +1 -127
- mcp_proxy_adapter/api/middleware/factory.py +0 -94
- mcp_proxy_adapter/api/middleware/logging.py +0 -112
- mcp_proxy_adapter/api/middleware/performance.py +0 -35
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +2 -98
- mcp_proxy_adapter/api/middleware/transport_middleware.py +0 -37
- mcp_proxy_adapter/api/middleware/unified_security.py +10 -10
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +0 -118
- mcp_proxy_adapter/api/openapi/__init__.py +21 -0
- mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
- mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
- mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
- mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
- mcp_proxy_adapter/api/schemas.py +0 -61
- mcp_proxy_adapter/api/tool_integration.py +0 -117
- mcp_proxy_adapter/api/tools.py +0 -46
- mcp_proxy_adapter/cli/__init__.py +12 -0
- mcp_proxy_adapter/cli/commands/__init__.py +15 -0
- mcp_proxy_adapter/cli/commands/client.py +100 -0
- mcp_proxy_adapter/cli/commands/config_generate.py +21 -0
- mcp_proxy_adapter/cli/commands/config_validate.py +36 -0
- mcp_proxy_adapter/cli/commands/generate.py +259 -0
- mcp_proxy_adapter/cli/commands/server.py +174 -0
- mcp_proxy_adapter/cli/commands/sets.py +128 -0
- mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
- mcp_proxy_adapter/cli/examples/__init__.py +8 -0
- mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
- mcp_proxy_adapter/cli/examples/https_token.py +96 -0
- mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
- mcp_proxy_adapter/cli/main.py +63 -0
- mcp_proxy_adapter/cli/parser.py +324 -0
- mcp_proxy_adapter/cli/validators.py +231 -0
- mcp_proxy_adapter/client/jsonrpc_client.py +406 -0
- mcp_proxy_adapter/client/proxy.py +45 -0
- mcp_proxy_adapter/commands/__init__.py +44 -28
- mcp_proxy_adapter/commands/auth_validation_command.py +7 -344
- mcp_proxy_adapter/commands/base.py +19 -43
- mcp_proxy_adapter/commands/builtin_commands.py +0 -75
- mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
- mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
- mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
- mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
- mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
- mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
- mcp_proxy_adapter/commands/catalog_manager.py +58 -928
- mcp_proxy_adapter/commands/cert_monitor_command.py +0 -88
- mcp_proxy_adapter/commands/certificate_management_command.py +0 -45
- mcp_proxy_adapter/commands/command_registry.py +172 -904
- mcp_proxy_adapter/commands/config_command.py +0 -28
- mcp_proxy_adapter/commands/dependency_container.py +1 -70
- mcp_proxy_adapter/commands/dependency_manager.py +0 -128
- mcp_proxy_adapter/commands/echo_command.py +0 -34
- mcp_proxy_adapter/commands/health_command.py +0 -3
- mcp_proxy_adapter/commands/help_command.py +0 -159
- mcp_proxy_adapter/commands/hooks.py +0 -137
- mcp_proxy_adapter/commands/key_management_command.py +0 -25
- mcp_proxy_adapter/commands/load_command.py +7 -78
- mcp_proxy_adapter/commands/plugins_command.py +0 -16
- mcp_proxy_adapter/commands/protocol_management_command.py +0 -28
- mcp_proxy_adapter/commands/proxy_registration_command.py +0 -88
- mcp_proxy_adapter/commands/queue_commands.py +750 -0
- mcp_proxy_adapter/commands/registration_status_command.py +0 -43
- mcp_proxy_adapter/commands/registry/__init__.py +18 -0
- mcp_proxy_adapter/commands/registry/command_info.py +103 -0
- mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
- mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
- mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
- mcp_proxy_adapter/commands/reload_command.py +0 -80
- mcp_proxy_adapter/commands/result.py +25 -77
- mcp_proxy_adapter/commands/role_test_command.py +0 -44
- mcp_proxy_adapter/commands/roles_management_command.py +0 -199
- mcp_proxy_adapter/commands/security_command.py +0 -30
- mcp_proxy_adapter/commands/settings_command.py +0 -68
- mcp_proxy_adapter/commands/ssl_setup_command.py +0 -42
- mcp_proxy_adapter/commands/token_management_command.py +0 -1
- mcp_proxy_adapter/commands/transport_management_command.py +0 -20
- mcp_proxy_adapter/commands/unload_command.py +0 -71
- mcp_proxy_adapter/config.py +15 -626
- mcp_proxy_adapter/core/__init__.py +5 -39
- mcp_proxy_adapter/core/app_factory.py +14 -36
- mcp_proxy_adapter/core/app_runner.py +0 -27
- mcp_proxy_adapter/core/auth_validator.py +1 -93
- mcp_proxy_adapter/core/certificate/__init__.py +20 -0
- mcp_proxy_adapter/core/certificate/certificate_creator.py +371 -0
- mcp_proxy_adapter/core/certificate/certificate_extractor.py +183 -0
- mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/certificate/certificate_validator.py +110 -0
- mcp_proxy_adapter/core/certificate/ssl_context_manager.py +70 -0
- mcp_proxy_adapter/core/certificate_utils.py +64 -903
- mcp_proxy_adapter/core/client.py +0 -6
- mcp_proxy_adapter/core/client_manager.py +0 -19
- mcp_proxy_adapter/core/client_security.py +0 -2
- mcp_proxy_adapter/core/config/__init__.py +18 -0
- mcp_proxy_adapter/core/config/config.py +195 -0
- mcp_proxy_adapter/core/config/config_factory.py +22 -0
- mcp_proxy_adapter/core/config/config_loader.py +66 -0
- mcp_proxy_adapter/core/config/feature_manager.py +31 -0
- mcp_proxy_adapter/core/config/simple_config.py +112 -0
- mcp_proxy_adapter/core/config/simple_config_generator.py +50 -0
- mcp_proxy_adapter/core/config/simple_config_validator.py +96 -0
- mcp_proxy_adapter/core/config_converter.py +0 -186
- mcp_proxy_adapter/core/config_validator.py +96 -1238
- mcp_proxy_adapter/core/errors.py +7 -42
- mcp_proxy_adapter/core/job_manager.py +54 -0
- mcp_proxy_adapter/core/logging.py +2 -22
- mcp_proxy_adapter/core/mtls_asgi.py +0 -20
- mcp_proxy_adapter/core/mtls_asgi_app.py +0 -12
- mcp_proxy_adapter/core/mtls_proxy.py +0 -80
- mcp_proxy_adapter/core/mtls_server.py +3 -173
- mcp_proxy_adapter/core/protocol_manager.py +1 -191
- mcp_proxy_adapter/core/proxy/__init__.py +22 -0
- mcp_proxy_adapter/core/proxy/auth_manager.py +27 -0
- mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +137 -0
- mcp_proxy_adapter/core/proxy/registration_client.py +60 -0
- mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
- mcp_proxy_adapter/core/proxy_client.py +0 -1
- mcp_proxy_adapter/core/proxy_registration.py +36 -912
- mcp_proxy_adapter/core/role_utils.py +0 -308
- mcp_proxy_adapter/core/security_adapter.py +1 -36
- mcp_proxy_adapter/core/security_factory.py +1 -150
- mcp_proxy_adapter/core/security_integration.py +0 -33
- mcp_proxy_adapter/core/server_adapter.py +1 -40
- mcp_proxy_adapter/core/server_engine.py +2 -173
- mcp_proxy_adapter/core/settings.py +0 -127
- mcp_proxy_adapter/core/signal_handler.py +0 -65
- mcp_proxy_adapter/core/ssl_utils.py +19 -137
- mcp_proxy_adapter/core/transport_manager.py +0 -151
- mcp_proxy_adapter/core/unified_config_adapter.py +1 -193
- mcp_proxy_adapter/core/utils.py +1 -182
- mcp_proxy_adapter/core/validation/__init__.py +21 -0
- mcp_proxy_adapter/core/validation/config_validator.py +211 -0
- mcp_proxy_adapter/core/validation/file_validator.py +73 -0
- mcp_proxy_adapter/core/validation/protocol_validator.py +191 -0
- mcp_proxy_adapter/core/validation/security_validator.py +58 -0
- mcp_proxy_adapter/core/validation/validation_result.py +27 -0
- mcp_proxy_adapter/custom_openapi.py +33 -652
- mcp_proxy_adapter/examples/bugfix_certificate_config.py +0 -23
- mcp_proxy_adapter/examples/check_config.py +0 -2
- mcp_proxy_adapter/examples/client_usage_example.py +164 -0
- mcp_proxy_adapter/examples/config_builder.py +13 -2
- mcp_proxy_adapter/examples/config_cli.py +0 -1
- mcp_proxy_adapter/examples/create_test_configs.py +0 -46
- mcp_proxy_adapter/examples/debug_request_state.py +0 -1
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +0 -47
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +0 -45
- mcp_proxy_adapter/examples/full_application/commands/echo_command.py +0 -12
- mcp_proxy_adapter/examples/full_application/commands/help_command.py +0 -12
- mcp_proxy_adapter/examples/full_application/commands/list_command.py +0 -7
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +0 -2
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +0 -59
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +0 -54
- mcp_proxy_adapter/examples/full_application/main.py +186 -150
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +0 -107
- mcp_proxy_adapter/examples/full_application/test_minimal_server.py +0 -24
- mcp_proxy_adapter/examples/full_application/test_server.py +0 -58
- mcp_proxy_adapter/examples/generate_config.py +65 -11
- mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
- mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
- mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
- mcp_proxy_adapter/examples/queue_server_example.py +85 -0
- mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
- mcp_proxy_adapter/examples/required_certificates.py +0 -2
- mcp_proxy_adapter/examples/run_full_test_suite.py +0 -29
- mcp_proxy_adapter/examples/run_proxy_server.py +31 -71
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +0 -27
- mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
- mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
- mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
- mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
- mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
- mcp_proxy_adapter/examples/security_test_client.py +24 -1075
- mcp_proxy_adapter/examples/setup/__init__.py +24 -0
- mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
- mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
- mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
- mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
- mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
- mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +133 -1425
- mcp_proxy_adapter/examples/test_config.py +0 -3
- mcp_proxy_adapter/examples/test_config_builder.py +25 -405
- mcp_proxy_adapter/examples/test_examples.py +0 -1
- mcp_proxy_adapter/examples/test_framework_complete.py +0 -2
- mcp_proxy_adapter/examples/test_mcp_server.py +0 -1
- mcp_proxy_adapter/examples/test_protocol_examples.py +0 -1
- mcp_proxy_adapter/examples/universal_client.py +0 -6
- mcp_proxy_adapter/examples/update_config_certificates.py +0 -1
- mcp_proxy_adapter/examples/validate_generator_compatibility.py +0 -1
- mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +0 -187
- mcp_proxy_adapter/integrations/__init__.py +25 -0
- mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
- mcp_proxy_adapter/main.py +70 -62
- mcp_proxy_adapter/openapi.py +0 -22
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/METADATA +2 -1
- mcp_proxy_adapter-6.9.29.dist-info/RECORD +235 -0
- {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/entry_points.txt +1 -1
- mcp_proxy_adapter-6.9.27.dist-info/RECORD +0 -149
- {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/top_level.txt +0 -0
mcp_proxy_adapter/api/app.py
CHANGED
|
@@ -2,120 +2,14 @@
|
|
|
2
2
|
Module for FastAPI application setup.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import ssl
|
|
7
|
-
import logging
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Any, Dict, List, Optional, Union
|
|
10
|
-
from contextlib import asynccontextmanager
|
|
11
|
-
import asyncio
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
12
6
|
|
|
13
|
-
|
|
7
|
+
from fastapi import FastAPI
|
|
14
8
|
|
|
15
|
-
from
|
|
16
|
-
from fastapi.responses import JSONResponse, Response
|
|
17
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
9
|
+
from .core import AppFactory, SSLContextFactory, RegistrationManager, LifespanManager
|
|
18
10
|
|
|
19
|
-
from mcp_proxy_adapter.api.handlers import (
|
|
20
|
-
execute_command,
|
|
21
|
-
handle_json_rpc,
|
|
22
|
-
handle_batch_json_rpc,
|
|
23
|
-
get_server_health,
|
|
24
|
-
get_commands_list,
|
|
25
|
-
)
|
|
26
|
-
from mcp_proxy_adapter.api.middleware import setup_middleware
|
|
27
|
-
from mcp_proxy_adapter.api.schemas import (
|
|
28
|
-
JsonRpcRequest,
|
|
29
|
-
JsonRpcSuccessResponse,
|
|
30
|
-
JsonRpcErrorResponse,
|
|
31
|
-
HealthResponse,
|
|
32
|
-
CommandListResponse,
|
|
33
|
-
APIToolDescription,
|
|
34
|
-
)
|
|
35
|
-
from mcp_proxy_adapter.api.tools import get_tool_description, execute_tool
|
|
36
|
-
from mcp_proxy_adapter.config import config
|
|
37
|
-
from mcp_proxy_adapter.core.errors import MicroserviceError, NotFoundError
|
|
38
|
-
from mcp_proxy_adapter.core.logging import get_global_logger, RequestLogger
|
|
39
|
-
from mcp_proxy_adapter.core.ssl_utils import SSLUtils
|
|
40
|
-
from mcp_proxy_adapter.commands.command_registry import registry
|
|
41
|
-
from mcp_proxy_adapter.custom_openapi import custom_openapi_with_fallback
|
|
42
11
|
|
|
43
12
|
|
|
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
|
-
get_global_logger().info(f"🔍 Using registration.protocol: {registration_protocol} -> {protocol}")
|
|
80
|
-
else:
|
|
81
|
-
# NO FALLBACK! Protocol must be explicitly specified
|
|
82
|
-
raise ValueError(
|
|
83
|
-
"registration.protocol is required in configuration. "
|
|
84
|
-
"Please specify protocol explicitly in proxy_registration section. "
|
|
85
|
-
"NO FALLBACK to server.protocol is allowed!"
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
# Determine host
|
|
89
|
-
if not public_host:
|
|
90
|
-
if server_host in ("0.0.0.0", "127.0.0.1"):
|
|
91
|
-
# Try to get hostname, fallback to docker host addr
|
|
92
|
-
try:
|
|
93
|
-
hostname = socket.gethostname()
|
|
94
|
-
# Use hostname if it's not localhost
|
|
95
|
-
if hostname and hostname not in ("localhost", "127.0.0.1"):
|
|
96
|
-
resolved_host = hostname
|
|
97
|
-
else:
|
|
98
|
-
resolved_host = os.getenv("DOCKER_HOST_ADDR", "172.17.0.1")
|
|
99
|
-
except Exception:
|
|
100
|
-
resolved_host = os.getenv("DOCKER_HOST_ADDR", "172.17.0.1")
|
|
101
|
-
else:
|
|
102
|
-
resolved_host = server_host
|
|
103
|
-
else:
|
|
104
|
-
resolved_host = public_host
|
|
105
|
-
|
|
106
|
-
# Determine port
|
|
107
|
-
resolved_port = public_port or server_port
|
|
108
|
-
|
|
109
|
-
# Build URL
|
|
110
|
-
server_url = f"{protocol}://{resolved_host}:{resolved_port}"
|
|
111
|
-
|
|
112
|
-
get_global_logger().info(
|
|
113
|
-
"🔍 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",
|
|
114
|
-
server_host, server_port, public_host, public_port, protocol, resolved_host, resolved_port, server_url
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
return server_url
|
|
118
|
-
|
|
119
13
|
|
|
120
14
|
def create_lifespan(config_path: Optional[str] = None, current_config: Optional[Dict[str, Any]] = None):
|
|
121
15
|
"""
|
|
@@ -128,168 +22,13 @@ def create_lifespan(config_path: Optional[str] = None, current_config: Optional[
|
|
|
128
22
|
Returns:
|
|
129
23
|
Lifespan context manager
|
|
130
24
|
"""
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
async def lifespan(app: FastAPI):
|
|
134
|
-
"""
|
|
135
|
-
Lifespan manager for the FastAPI application. Handles startup and shutdown events.
|
|
136
|
-
"""
|
|
137
|
-
# Startup events
|
|
138
|
-
from mcp_proxy_adapter.commands.command_registry import registry
|
|
139
|
-
from mcp_proxy_adapter.core.proxy_registration import (
|
|
140
|
-
register_with_proxy,
|
|
141
|
-
unregister_from_proxy,
|
|
142
|
-
initialize_proxy_registration,
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
# Proxy registration manager will be initialized in registry.reload_system()
|
|
146
|
-
# after all commands are loaded to ensure complete command schema
|
|
147
|
-
|
|
148
|
-
# Compute server_url EARLY and inject into registration manager so
|
|
149
|
-
# that reload_system (which may perform registration) uses the correct
|
|
150
|
-
# externally reachable address.
|
|
151
|
-
# Use current_config from closure or fallback to global config
|
|
152
|
-
config_to_use = current_config
|
|
153
|
-
if config_to_use is None:
|
|
154
|
-
# Fallback: try to get config from global instance
|
|
155
|
-
try:
|
|
156
|
-
from mcp_proxy_adapter.config import get_config
|
|
157
|
-
config_to_use = get_config().get_all()
|
|
158
|
-
except Exception:
|
|
159
|
-
config_to_use = {}
|
|
160
|
-
|
|
161
|
-
server_config = config_to_use.get("server")
|
|
162
|
-
if not server_config:
|
|
163
|
-
raise ValueError("server configuration is required")
|
|
164
|
-
server_host = server_config.get("host")
|
|
165
|
-
server_port = server_config.get("port")
|
|
166
|
-
if not server_host:
|
|
167
|
-
raise ValueError("server.host is required")
|
|
168
|
-
if not server_port:
|
|
169
|
-
raise ValueError("server.port is required")
|
|
170
|
-
|
|
171
|
-
# Check port availability BEFORE starting registration manager
|
|
172
|
-
from mcp_proxy_adapter.core.utils import check_port_availability, handle_port_conflict
|
|
173
|
-
|
|
174
|
-
print(f"🔍 Checking external server port availability: {server_host}:{server_port}")
|
|
175
|
-
if not check_port_availability(server_host, server_port):
|
|
176
|
-
print(f"❌ CRITICAL: External server port {server_port} is occupied")
|
|
177
|
-
handle_port_conflict(server_host, server_port)
|
|
178
|
-
return # Exit the function immediately
|
|
179
|
-
print(f"✅ External server port {server_port} is available")
|
|
180
|
-
|
|
181
|
-
# Determine registration URL using unified logic
|
|
182
|
-
early_server_url = _determine_registration_url(config_to_use)
|
|
183
|
-
try:
|
|
184
|
-
from mcp_proxy_adapter.core.proxy_registration import (
|
|
185
|
-
register_with_proxy,
|
|
186
|
-
unregister_from_proxy,
|
|
187
|
-
initialize_proxy_registration,
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
# Initialize proxy registration
|
|
191
|
-
initialize_proxy_registration(config_to_use)
|
|
192
|
-
get_global_logger().info(
|
|
193
|
-
"🔍 Initialized proxy registration with server_url: %s",
|
|
194
|
-
early_server_url,
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
except Exception as e:
|
|
198
|
-
get_global_logger().error(f"Failed to initialize async registration: {e}")
|
|
199
|
-
|
|
200
|
-
# Initialize system using unified logic (may perform registration)
|
|
201
|
-
# Set global config for reload_system
|
|
202
|
-
from mcp_proxy_adapter.config import Config
|
|
203
|
-
config_obj = Config()
|
|
204
|
-
config_obj.config_data = config_to_use
|
|
205
|
-
|
|
206
|
-
# Set global config for command registry
|
|
207
|
-
from mcp_proxy_adapter.config import get_config
|
|
208
|
-
global_config = get_config()
|
|
209
|
-
global_config.config_data = config_to_use
|
|
210
|
-
|
|
211
|
-
if config_path:
|
|
212
|
-
init_result = await registry.reload_system(config_path=config_path, config_obj=config_obj)
|
|
213
|
-
else:
|
|
214
|
-
init_result = await registry.reload_system(config_obj=config_obj)
|
|
215
|
-
|
|
216
|
-
get_global_logger().info(
|
|
217
|
-
f"Application started with {init_result['total_commands']} commands registered"
|
|
218
|
-
)
|
|
219
|
-
get_global_logger().info(f"System initialization result: {init_result}")
|
|
220
|
-
|
|
221
|
-
# Proxy registration manager is already initialized in registry.reload_system()
|
|
222
|
-
|
|
223
|
-
# Recompute registration URL AFTER config reload using final config
|
|
224
|
-
try:
|
|
225
|
-
final_config = config_to_use # config_to_use is already a dict
|
|
226
|
-
server_config = final_config.get("server", {})
|
|
227
|
-
server_host = server_config.get("host", "0.0.0.0")
|
|
228
|
-
server_port = server_config.get("port", 8000)
|
|
229
|
-
|
|
230
|
-
# Determine registration URL using unified logic
|
|
231
|
-
server_url = _determine_registration_url(final_config)
|
|
232
|
-
|
|
233
|
-
# Update proxy registration with final server URL
|
|
234
|
-
try:
|
|
235
|
-
get_global_logger().info(f"🔍 Updated proxy registration with final server_url: {server_url}")
|
|
236
|
-
|
|
237
|
-
except Exception as e:
|
|
238
|
-
get_global_logger().error(f"Failed to update proxy registration: {e}")
|
|
239
|
-
|
|
240
|
-
try:
|
|
241
|
-
print("🔍 Registration server_url resolved to (print):", server_url)
|
|
242
|
-
except Exception:
|
|
243
|
-
pass
|
|
244
|
-
except Exception as e:
|
|
245
|
-
get_global_logger().error(f"Failed to recompute registration URL: {e}")
|
|
246
|
-
server_url = early_server_url
|
|
247
|
-
|
|
248
|
-
# Proxy registration is now handled in registry.reload_system()
|
|
249
|
-
# after all commands are loaded, ensuring complete command schema
|
|
250
|
-
get_global_logger().info("ℹ️ Proxy registration will be handled after command loading completes")
|
|
251
|
-
|
|
252
|
-
# Add delayed registration task to allow server to fully start
|
|
253
|
-
async def delayed_registration():
|
|
254
|
-
"""Delayed registration to ensure server is fully started."""
|
|
255
|
-
await asyncio.sleep(2) # Wait for server to start listening
|
|
256
|
-
get_global_logger().info("🔄 Attempting delayed proxy registration after server startup")
|
|
257
|
-
try:
|
|
258
|
-
from mcp_proxy_adapter.core.proxy_registration import register_with_proxy, initialize_proxy_registration
|
|
259
|
-
# Ensure proxy registration manager is initialized
|
|
260
|
-
initialize_proxy_registration(config_to_use)
|
|
261
|
-
success = await register_with_proxy(server_url)
|
|
262
|
-
if success:
|
|
263
|
-
get_global_logger().info("✅ Delayed proxy registration successful")
|
|
264
|
-
else:
|
|
265
|
-
get_global_logger().warning("⚠️ Delayed proxy registration failed")
|
|
266
|
-
except Exception as e:
|
|
267
|
-
get_global_logger().error(f"❌ Delayed proxy registration error: {e}")
|
|
268
|
-
|
|
269
|
-
asyncio.create_task(delayed_registration())
|
|
270
|
-
|
|
271
|
-
yield # Application is running
|
|
272
|
-
|
|
273
|
-
# Shutdown events
|
|
274
|
-
get_global_logger().info("Application shutting down")
|
|
275
|
-
|
|
276
|
-
# Stop proxy registration (this will also unregister)
|
|
277
|
-
try:
|
|
278
|
-
unregistration_success = await unregister_from_proxy()
|
|
279
|
-
if unregistration_success:
|
|
280
|
-
get_global_logger().info("✅ Proxy unregistration completed successfully")
|
|
281
|
-
else:
|
|
282
|
-
get_global_logger().warning("⚠️ Proxy unregistration failed or was disabled")
|
|
283
|
-
|
|
284
|
-
except Exception as e:
|
|
285
|
-
get_global_logger().error(f"❌ Failed to stop proxy registration: {e}")
|
|
286
|
-
|
|
287
|
-
return lifespan
|
|
25
|
+
lifespan_manager = LifespanManager()
|
|
26
|
+
return lifespan_manager.create_lifespan(config_path, current_config)
|
|
288
27
|
|
|
289
28
|
|
|
290
29
|
def create_ssl_context(
|
|
291
30
|
app_config: Optional[Dict[str, Any]] = None
|
|
292
|
-
) -> Optional[
|
|
31
|
+
) -> Optional[Any]:
|
|
293
32
|
"""
|
|
294
33
|
Create SSL context based on configuration.
|
|
295
34
|
|
|
@@ -299,57 +38,8 @@ def create_ssl_context(
|
|
|
299
38
|
Returns:
|
|
300
39
|
SSL context if SSL is enabled and properly configured, None otherwise
|
|
301
40
|
"""
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
# Check SSL configuration from new structure
|
|
305
|
-
protocol = current_config.get("server", {}).get("protocol", "http")
|
|
306
|
-
verify_client = current_config.get("transport", {}).get("verify_client", False)
|
|
307
|
-
ssl_enabled = protocol in ["https", "mtls"] or verify_client
|
|
308
|
-
|
|
309
|
-
if not ssl_enabled:
|
|
310
|
-
get_global_logger().info("SSL is disabled in configuration")
|
|
311
|
-
return None
|
|
312
|
-
|
|
313
|
-
# Get certificate paths from configuration
|
|
314
|
-
cert_file = current_config.get("transport", {}).get("cert_file")
|
|
315
|
-
key_file = current_config.get("transport", {}).get("key_file")
|
|
316
|
-
ca_cert = current_config.get("transport", {}).get("ca_cert")
|
|
317
|
-
|
|
318
|
-
# Convert relative paths to absolute paths
|
|
319
|
-
if cert_file and not Path(cert_file).is_absolute():
|
|
320
|
-
project_root = Path(__file__).parent.parent.parent
|
|
321
|
-
cert_file = str(project_root / cert_file)
|
|
322
|
-
if key_file and not Path(key_file).is_absolute():
|
|
323
|
-
project_root = Path(__file__).parent.parent.parent
|
|
324
|
-
key_file = str(project_root / key_file)
|
|
325
|
-
if ca_cert and not Path(ca_cert).is_absolute():
|
|
326
|
-
project_root = Path(__file__).parent.parent.parent
|
|
327
|
-
ca_cert = str(project_root / ca_cert)
|
|
328
|
-
|
|
329
|
-
if not cert_file or not key_file:
|
|
330
|
-
get_global_logger().warning("SSL enabled but certificate or key file not specified")
|
|
331
|
-
return None
|
|
332
|
-
|
|
333
|
-
try:
|
|
334
|
-
# Create SSL context using SSLUtils
|
|
335
|
-
ssl_context = SSLUtils.create_ssl_context(
|
|
336
|
-
cert_file=cert_file,
|
|
337
|
-
key_file=key_file,
|
|
338
|
-
ca_cert=ca_cert,
|
|
339
|
-
verify_client=current_config.get("transport", {}).get("verify_client", False),
|
|
340
|
-
cipher_suites=[],
|
|
341
|
-
min_tls_version="1.2",
|
|
342
|
-
max_tls_version="1.3",
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
get_global_logger().info(
|
|
346
|
-
f"SSL context created successfully for mode: https_only"
|
|
347
|
-
)
|
|
348
|
-
return ssl_context
|
|
349
|
-
|
|
350
|
-
except Exception as e:
|
|
351
|
-
get_global_logger().error(f"Failed to create SSL context: {e}")
|
|
352
|
-
return None
|
|
41
|
+
ssl_factory = SSLContextFactory()
|
|
42
|
+
return ssl_factory.create_ssl_context(app_config)
|
|
353
43
|
|
|
354
44
|
|
|
355
45
|
def create_app(
|
|
@@ -371,599 +61,6 @@ def create_app(
|
|
|
371
61
|
|
|
372
62
|
Returns:
|
|
373
63
|
Configured FastAPI application.
|
|
374
|
-
|
|
375
|
-
Raises:
|
|
376
|
-
SystemExit: If authentication is enabled but required files are missing (security issue)
|
|
377
64
|
"""
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if hasattr(app_config, "get_all"):
|
|
381
|
-
current_config = app_config.get_all()
|
|
382
|
-
elif hasattr(app_config, "keys"):
|
|
383
|
-
current_config = app_config
|
|
384
|
-
else:
|
|
385
|
-
# If app_config is not a dict-like object, use it as is
|
|
386
|
-
current_config = app_config
|
|
387
|
-
else:
|
|
388
|
-
# If no app_config provided, try to get global config
|
|
389
|
-
try:
|
|
390
|
-
from mcp_proxy_adapter.config import get_config
|
|
391
|
-
current_config = get_config().get_all()
|
|
392
|
-
except Exception:
|
|
393
|
-
# If global config is not available, create empty config
|
|
394
|
-
current_config = {}
|
|
395
|
-
|
|
396
|
-
# Debug: Check what config is passed to create_app
|
|
397
|
-
if app_config:
|
|
398
|
-
if hasattr(app_config, "keys"):
|
|
399
|
-
print(
|
|
400
|
-
f"🔍 Debug: create_app received app_config keys: {list(app_config.keys())}"
|
|
401
|
-
)
|
|
402
|
-
# Debug SSL configuration
|
|
403
|
-
protocol = app_config.get("server", {}).get("protocol", "http")
|
|
404
|
-
verify_client = app_config.get("transport", {}).get("verify_client", False)
|
|
405
|
-
ssl_enabled = protocol in ["https", "mtls"] or verify_client
|
|
406
|
-
print(f"🔍 Debug: create_app SSL config: enabled={ssl_enabled}")
|
|
407
|
-
print(f"🔍 Debug: create_app protocol: {protocol}")
|
|
408
|
-
print(f"🔍 Debug: create_app verify_client: {verify_client}")
|
|
409
|
-
else:
|
|
410
|
-
print(f"🔍 Debug: create_app received app_config type: {type(app_config)}")
|
|
411
|
-
else:
|
|
412
|
-
print("🔍 Debug: create_app received no app_config, using global config")
|
|
413
|
-
|
|
414
|
-
# Security check: Validate configuration strictly at startup (fail-fast)
|
|
415
|
-
try:
|
|
416
|
-
from mcp_proxy_adapter.core.config_validator import ConfigValidator
|
|
417
|
-
|
|
418
|
-
_validator = ConfigValidator()
|
|
419
|
-
_validator.config_data = current_config
|
|
420
|
-
_validation_results = _validator.validate_config()
|
|
421
|
-
errors = [r for r in _validation_results if r.level == "error"]
|
|
422
|
-
warnings = [r for r in _validation_results if r.level == "warning"]
|
|
423
|
-
|
|
424
|
-
if errors:
|
|
425
|
-
get_global_logger().critical("CRITICAL CONFIG ERROR: Invalid configuration at startup:")
|
|
426
|
-
for _e in errors:
|
|
427
|
-
get_global_logger().critical(f" - {_e.message}")
|
|
428
|
-
raise SystemExit(1)
|
|
429
|
-
for _w in warnings:
|
|
430
|
-
get_global_logger().warning(f"Config warning: {_w.message}")
|
|
431
|
-
except Exception as _ex:
|
|
432
|
-
get_global_logger().error(f"Failed to run startup configuration validation: {_ex}")
|
|
433
|
-
|
|
434
|
-
# Security check: Validate all authentication configurations before startup (legacy checks kept for compatibility)
|
|
435
|
-
security_errors = []
|
|
436
|
-
|
|
437
|
-
print(f"🔍 Debug: current_config keys: {list(current_config.keys())}")
|
|
438
|
-
if "security" in current_config:
|
|
439
|
-
print(f"🔍 Debug: security config: {current_config['security']}")
|
|
440
|
-
if "roles" in current_config:
|
|
441
|
-
print(f"🔍 Debug: roles config: {current_config['roles']}")
|
|
442
|
-
|
|
443
|
-
# Check security framework configuration only if enabled
|
|
444
|
-
security_config = current_config.get("security", {})
|
|
445
|
-
if security_config.get("enabled", False):
|
|
446
|
-
# Validate security framework configuration
|
|
447
|
-
from mcp_proxy_adapter.core.unified_config_adapter import UnifiedConfigAdapter
|
|
448
|
-
|
|
449
|
-
adapter = UnifiedConfigAdapter()
|
|
450
|
-
validation_result = adapter.validate_configuration(current_config)
|
|
451
|
-
|
|
452
|
-
if not validation_result.is_valid:
|
|
453
|
-
security_errors.extend(validation_result.errors)
|
|
454
|
-
|
|
455
|
-
# Check SSL configuration within security framework
|
|
456
|
-
ssl_config = security_config.get("ssl", {})
|
|
457
|
-
if ssl_config.get("enabled", False):
|
|
458
|
-
cert_file = ssl_config.get("cert_file")
|
|
459
|
-
key_file = ssl_config.get("key_file")
|
|
460
|
-
|
|
461
|
-
print(
|
|
462
|
-
f"🔍 Debug: api/app.py security.ssl: cert_file={cert_file}, key_file={key_file}"
|
|
463
|
-
)
|
|
464
|
-
print(
|
|
465
|
-
f"🔍 Debug: api/app.py security.ssl: cert_file exists={Path(cert_file).exists() if cert_file else 'None'}"
|
|
466
|
-
)
|
|
467
|
-
print(
|
|
468
|
-
f"🔍 Debug: api/app.py security.ssl: key_file exists={Path(key_file).exists() if key_file else 'None'}"
|
|
469
|
-
)
|
|
470
|
-
|
|
471
|
-
if cert_file and not Path(cert_file).exists():
|
|
472
|
-
security_errors.append(
|
|
473
|
-
f"SSL is enabled but certificate file not found: {cert_file}"
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
if key_file and not Path(key_file).exists():
|
|
477
|
-
security_errors.append(
|
|
478
|
-
f"SSL is enabled but private key file not found: {key_file}"
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
# Check mTLS configuration
|
|
482
|
-
ca_cert_file = ssl_config.get("ca_cert_file")
|
|
483
|
-
if ca_cert_file and not Path(ca_cert_file).exists():
|
|
484
|
-
security_errors.append(
|
|
485
|
-
f"mTLS is enabled but CA certificate file not found: {ca_cert_file}"
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
# Legacy configuration checks for backward compatibility
|
|
489
|
-
roles_config = current_config.get("roles", {})
|
|
490
|
-
print(f"🔍 Debug: roles_config = {roles_config}")
|
|
491
|
-
if roles_config.get("enabled", False):
|
|
492
|
-
roles_config_path = roles_config.get("config_file", "schemas/roles_schema.json")
|
|
493
|
-
print(f"🔍 Debug: Checking roles file: {roles_config_path}")
|
|
494
|
-
if not Path(roles_config_path).exists():
|
|
495
|
-
security_errors.append(
|
|
496
|
-
f"Roles are enabled but schema file not found: {roles_config_path}"
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
# Check new security framework permissions configuration
|
|
500
|
-
security_config = current_config.get("security", {})
|
|
501
|
-
permissions_config = security_config.get("permissions", {})
|
|
502
|
-
if permissions_config.get("enabled", False):
|
|
503
|
-
roles_file = permissions_config.get("roles_file")
|
|
504
|
-
if roles_file and not Path(roles_file).exists():
|
|
505
|
-
security_errors.append(
|
|
506
|
-
f"Permissions are enabled but roles file not found: {roles_file}"
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
legacy_ssl_config = current_config.get("ssl", {})
|
|
510
|
-
if legacy_ssl_config.get("enabled", False):
|
|
511
|
-
# Check SSL certificate files
|
|
512
|
-
cert_file = legacy_ssl_config.get("cert_file")
|
|
513
|
-
key_file = legacy_ssl_config.get("key_file")
|
|
514
|
-
|
|
515
|
-
print(
|
|
516
|
-
f"🔍 Debug: api/app.py legacy.ssl: cert_file={cert_file}, key_file={key_file}"
|
|
517
|
-
)
|
|
518
|
-
print(
|
|
519
|
-
f"🔍 Debug: api/app.py legacy.ssl: cert_file exists={Path(cert_file).exists() if cert_file else 'None'}"
|
|
520
|
-
)
|
|
521
|
-
print(
|
|
522
|
-
f"🔍 Debug: api/app.py legacy.ssl: key_file exists={Path(key_file).exists() if key_file else 'None'}"
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
if cert_file and not Path(cert_file).exists():
|
|
526
|
-
security_errors.append(
|
|
527
|
-
f"Legacy SSL is enabled but certificate file not found: {cert_file}"
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
if key_file and not Path(key_file).exists():
|
|
531
|
-
security_errors.append(
|
|
532
|
-
f"Legacy SSL is enabled but private key file not found: {key_file}"
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
# Check mTLS configuration
|
|
536
|
-
if legacy_ssl_config.get("mode") == "mtls":
|
|
537
|
-
ca_cert = legacy_ssl_config.get("ca_cert")
|
|
538
|
-
if ca_cert and not Path(ca_cert).exists():
|
|
539
|
-
security_errors.append(
|
|
540
|
-
f"Legacy mTLS is enabled but CA certificate file not found: {ca_cert}"
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
# Check token authentication configuration
|
|
544
|
-
token_auth_config = legacy_ssl_config.get("token_auth", {})
|
|
545
|
-
if token_auth_config.get("enabled", False):
|
|
546
|
-
tokens_file = token_auth_config.get("tokens_file", "tokens.json")
|
|
547
|
-
if not Path(tokens_file).exists():
|
|
548
|
-
security_errors.append(
|
|
549
|
-
f"Token authentication is enabled but tokens file not found: {tokens_file}"
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
# Check general authentication
|
|
553
|
-
if current_config.get("auth_enabled", False):
|
|
554
|
-
# If auth is enabled, check if any authentication method is properly configured
|
|
555
|
-
ssl_enabled = legacy_ssl_config.get("enabled", False)
|
|
556
|
-
roles_enabled = roles_config.get("enabled", False)
|
|
557
|
-
token_auth_enabled = token_auth_config.get("enabled", False)
|
|
558
|
-
|
|
559
|
-
if not (ssl_enabled or roles_enabled or token_auth_enabled):
|
|
560
|
-
security_errors.append(
|
|
561
|
-
"Authentication is enabled but no authentication method is properly configured"
|
|
562
|
-
)
|
|
563
|
-
|
|
564
|
-
# If there are security errors, block startup
|
|
565
|
-
if security_errors:
|
|
566
|
-
get_global_logger().critical(
|
|
567
|
-
"CRITICAL SECURITY ERROR: Authentication configuration issues detected:"
|
|
568
|
-
)
|
|
569
|
-
for error in security_errors:
|
|
570
|
-
get_global_logger().critical(f" - {error}")
|
|
571
|
-
get_global_logger().critical("Server startup blocked for security reasons.")
|
|
572
|
-
get_global_logger().critical(
|
|
573
|
-
"Please fix authentication configuration or disable authentication features."
|
|
574
|
-
)
|
|
575
|
-
raise SystemExit(1)
|
|
576
|
-
|
|
577
|
-
# Use provided parameters or defaults
|
|
578
|
-
app_title = title or "MCP Proxy Adapter"
|
|
579
|
-
app_description = description or "JSON-RPC API for interacting with MCP Proxy"
|
|
580
|
-
app_version = version or "1.0.0"
|
|
581
|
-
|
|
582
|
-
# Create application
|
|
583
|
-
app = FastAPI(
|
|
584
|
-
title=app_title,
|
|
585
|
-
description=app_description,
|
|
586
|
-
version=app_version,
|
|
587
|
-
docs_url="/docs",
|
|
588
|
-
redoc_url="/redoc",
|
|
589
|
-
lifespan=create_lifespan(config_path, current_config),
|
|
590
|
-
)
|
|
591
|
-
|
|
592
|
-
# CRITICAL FIX: Register commands immediately during app creation
|
|
593
|
-
# This ensures commands are available before the server starts accepting requests
|
|
594
|
-
try:
|
|
595
|
-
from mcp_proxy_adapter.commands.builtin_commands import register_builtin_commands
|
|
596
|
-
get_global_logger().info("Registering built-in commands during app creation...")
|
|
597
|
-
registered_count = register_builtin_commands()
|
|
598
|
-
get_global_logger().info(f"Registered {registered_count} built-in commands during app creation")
|
|
599
|
-
except Exception as e:
|
|
600
|
-
get_global_logger().error(f"Failed to register built-in commands during app creation: {e}")
|
|
601
|
-
# Don't fail app creation, but log the error
|
|
602
|
-
|
|
603
|
-
# Configure CORS
|
|
604
|
-
app.add_middleware(
|
|
605
|
-
CORSMiddleware,
|
|
606
|
-
allow_origins=["*"], # In production, specify concrete domains
|
|
607
|
-
allow_credentials=True,
|
|
608
|
-
allow_methods=["*"],
|
|
609
|
-
allow_headers=["*"],
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
# Add request logging middleware for debugging
|
|
613
|
-
@app.middleware("http")
|
|
614
|
-
async def debug_request_middleware(request: Request, call_next):
|
|
615
|
-
get_global_logger().debug(f"FastAPI Request START: {request.method} {request.url.path}")
|
|
616
|
-
try:
|
|
617
|
-
response = await call_next(request)
|
|
618
|
-
get_global_logger().debug(f"FastAPI Request COMPLETED: {response.status_code}")
|
|
619
|
-
return response
|
|
620
|
-
except Exception as e:
|
|
621
|
-
get_global_logger().error(f"FastAPI Request ERROR: {e}", exc_info=True)
|
|
622
|
-
raise
|
|
623
|
-
|
|
624
|
-
# Setup middleware using the new middleware package
|
|
625
|
-
setup_middleware(app, current_config)
|
|
626
|
-
|
|
627
|
-
# Add request logging middleware
|
|
628
|
-
# @app.middleware("http")
|
|
629
|
-
# async def log_requests(request: Request, call_next):
|
|
630
|
-
# get_global_logger().info(f"🔍 REQUEST LOG: {request.method} {request.url.path}")
|
|
631
|
-
# get_global_logger().info(f"🔍 REQUEST LOG: Headers: {dict(request.headers)}")
|
|
632
|
-
# get_global_logger().info(f"🔍 REQUEST LOG: Client: {request.client}")
|
|
633
|
-
# response = await call_next(request)
|
|
634
|
-
# get_global_logger().info(f"🔍 RESPONSE LOG: Status: {response.status_code}")
|
|
635
|
-
# return response
|
|
636
|
-
|
|
637
|
-
# Use custom OpenAPI schema
|
|
638
|
-
app.openapi = lambda: custom_openapi_with_fallback(app)
|
|
639
|
-
|
|
640
|
-
# Explicit endpoint for OpenAPI schema
|
|
641
|
-
@app.get("/openapi.json")
|
|
642
|
-
async def get_openapi_schema():
|
|
643
|
-
"""
|
|
644
|
-
Returns optimized OpenAPI schema compatible with MCP-Proxy.
|
|
645
|
-
"""
|
|
646
|
-
return custom_openapi_with_fallback(app)
|
|
647
|
-
|
|
648
|
-
# JSON-RPC handler
|
|
649
|
-
@app.post(
|
|
650
|
-
"/api/jsonrpc",
|
|
651
|
-
response_model=Union[
|
|
652
|
-
JsonRpcSuccessResponse,
|
|
653
|
-
JsonRpcErrorResponse,
|
|
654
|
-
List[Union[JsonRpcSuccessResponse, JsonRpcErrorResponse]],
|
|
655
|
-
],
|
|
656
|
-
)
|
|
657
|
-
async def jsonrpc_endpoint(
|
|
658
|
-
request: Request,
|
|
659
|
-
request_data: Union[Dict[str, Any], List[Dict[str, Any]]] = Body(...),
|
|
660
|
-
):
|
|
661
|
-
"""
|
|
662
|
-
Endpoint for handling JSON-RPC requests.
|
|
663
|
-
Supports both single and batch requests.
|
|
664
|
-
"""
|
|
665
|
-
# Get request_id from middleware state
|
|
666
|
-
request_id = getattr(request.state, "request_id", None)
|
|
667
|
-
|
|
668
|
-
# Create request get_global_logger() for this endpoint
|
|
669
|
-
req_logger = RequestLogger(__name__, request_id) if request_id else get_global_logger()
|
|
670
|
-
|
|
671
|
-
# Check if it's a batch request
|
|
672
|
-
if isinstance(request_data, list):
|
|
673
|
-
# Process batch request
|
|
674
|
-
if len(request_data) == 0:
|
|
675
|
-
# Empty batch request is invalid
|
|
676
|
-
req_logger.warning("Invalid Request: Empty batch request")
|
|
677
|
-
return JSONResponse(
|
|
678
|
-
status_code=400,
|
|
679
|
-
content={
|
|
680
|
-
"jsonrpc": "2.0",
|
|
681
|
-
"error": {
|
|
682
|
-
"code": -32600,
|
|
683
|
-
"message": "Invalid Request. Empty batch request",
|
|
684
|
-
},
|
|
685
|
-
"id": None,
|
|
686
|
-
},
|
|
687
|
-
)
|
|
688
|
-
return await handle_batch_json_rpc(request_data, request)
|
|
689
|
-
else:
|
|
690
|
-
# Process single request
|
|
691
|
-
return await handle_json_rpc(request_data, request_id, request)
|
|
692
|
-
|
|
693
|
-
# Command execution endpoint (/cmd)
|
|
694
|
-
@app.post("/cmd")
|
|
695
|
-
async def cmd_endpoint(request: Request, command_data: Dict[str, Any] = Body(...)):
|
|
696
|
-
"""
|
|
697
|
-
Universal endpoint for executing commands.
|
|
698
|
-
Supports two formats:
|
|
699
|
-
1. CommandRequest:
|
|
700
|
-
{
|
|
701
|
-
"command": "command_name",
|
|
702
|
-
"params": {
|
|
703
|
-
// Command parameters
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
2. JSON-RPC:
|
|
708
|
-
{
|
|
709
|
-
"jsonrpc": "2.0",
|
|
710
|
-
"method": "command_name",
|
|
711
|
-
"params": {
|
|
712
|
-
// Command parameters
|
|
713
|
-
},
|
|
714
|
-
"id": 123
|
|
715
|
-
}
|
|
716
|
-
"""
|
|
717
|
-
# Get request_id from middleware state
|
|
718
|
-
request_id = getattr(request.state, "request_id", None)
|
|
719
|
-
|
|
720
|
-
# Create request get_global_logger() for this endpoint
|
|
721
|
-
req_logger = RequestLogger(__name__, request_id) if request_id else get_global_logger()
|
|
722
|
-
|
|
723
|
-
try:
|
|
724
|
-
# Determine request format (CommandRequest or JSON-RPC)
|
|
725
|
-
if "jsonrpc" in command_data and "method" in command_data:
|
|
726
|
-
# JSON-RPC format
|
|
727
|
-
return await handle_json_rpc(command_data, request_id, request)
|
|
728
|
-
|
|
729
|
-
# CommandRequest format
|
|
730
|
-
if "command" not in command_data:
|
|
731
|
-
req_logger.warning("Missing required field 'command'")
|
|
732
|
-
return JSONResponse(
|
|
733
|
-
status_code=200,
|
|
734
|
-
content={
|
|
735
|
-
"error": {
|
|
736
|
-
"code": -32600,
|
|
737
|
-
"message": "Отсутствует обязательное поле 'command'",
|
|
738
|
-
}
|
|
739
|
-
},
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
command_name = command_data["command"]
|
|
743
|
-
params = command_data.get("params", {})
|
|
744
|
-
|
|
745
|
-
req_logger.debug(
|
|
746
|
-
f"Executing command via /cmd: {command_name}, params: {params}"
|
|
747
|
-
)
|
|
748
|
-
|
|
749
|
-
# Check if command exists
|
|
750
|
-
if not registry.command_exists(command_name):
|
|
751
|
-
req_logger.warning(f"Command '{command_name}' not found")
|
|
752
|
-
return JSONResponse(
|
|
753
|
-
status_code=200,
|
|
754
|
-
content={
|
|
755
|
-
"error": {
|
|
756
|
-
"code": -32601,
|
|
757
|
-
"message": f"Команда '{command_name}' не найдена",
|
|
758
|
-
}
|
|
759
|
-
},
|
|
760
|
-
)
|
|
761
|
-
|
|
762
|
-
# Execute command
|
|
763
|
-
try:
|
|
764
|
-
result = await execute_command(
|
|
765
|
-
command_name, params, request_id, request
|
|
766
|
-
)
|
|
767
|
-
return {"result": result}
|
|
768
|
-
except MicroserviceError as e:
|
|
769
|
-
# Handle command execution errors
|
|
770
|
-
req_logger.error(f"Error executing command '{command_name}': {str(e)}")
|
|
771
|
-
return JSONResponse(status_code=200, content={"error": e.to_dict()})
|
|
772
|
-
except NotFoundError as e:
|
|
773
|
-
# Специальная обработка для help-команды: возвращаем result с пустым commands и error
|
|
774
|
-
if command_name == "help":
|
|
775
|
-
return {
|
|
776
|
-
"result": {
|
|
777
|
-
"success": False,
|
|
778
|
-
"commands": {},
|
|
779
|
-
"error": str(e),
|
|
780
|
-
"note": 'To get detailed information about a specific command, call help with parameter: POST /cmd {"command": "help", "params": {"cmdname": "<command_name>"}}',
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
# Для остальных команд — стандартная ошибка
|
|
784
|
-
return JSONResponse(
|
|
785
|
-
status_code=200,
|
|
786
|
-
content={"error": {"code": e.code, "message": str(e)}},
|
|
787
|
-
)
|
|
788
|
-
|
|
789
|
-
except json.JSONDecodeError:
|
|
790
|
-
req_logger.error("JSON decode error")
|
|
791
|
-
return JSONResponse(
|
|
792
|
-
status_code=200,
|
|
793
|
-
content={"error": {"code": -32700, "message": "Parse error"}},
|
|
794
|
-
)
|
|
795
|
-
except Exception as e:
|
|
796
|
-
req_logger.exception(f"Unexpected error: {str(e)}")
|
|
797
|
-
return JSONResponse(
|
|
798
|
-
status_code=200,
|
|
799
|
-
content={
|
|
800
|
-
"error": {
|
|
801
|
-
"code": -32603,
|
|
802
|
-
"message": "Internal error",
|
|
803
|
-
"data": {"details": str(e)},
|
|
804
|
-
}
|
|
805
|
-
},
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
# Direct command call
|
|
809
|
-
@app.post("/api/command/{command_name}")
|
|
810
|
-
async def command_endpoint(
|
|
811
|
-
request: Request, command_name: str, params: Dict[str, Any] = Body(default={})
|
|
812
|
-
):
|
|
813
|
-
"""
|
|
814
|
-
Endpoint for direct command call.
|
|
815
|
-
"""
|
|
816
|
-
# Get request_id from middleware state
|
|
817
|
-
request_id = getattr(request.state, "request_id", None)
|
|
818
|
-
|
|
819
|
-
try:
|
|
820
|
-
result = await execute_command(command_name, params, request_id, request)
|
|
821
|
-
return result
|
|
822
|
-
except MicroserviceError as e:
|
|
823
|
-
# Convert to proper HTTP status code
|
|
824
|
-
status_code = 400 if e.code < 0 else e.code
|
|
825
|
-
return JSONResponse(status_code=status_code, content=e.to_dict())
|
|
826
|
-
|
|
827
|
-
# Server health check
|
|
828
|
-
@app.get("/health", operation_id="health_check")
|
|
829
|
-
async def health_endpoint():
|
|
830
|
-
"""
|
|
831
|
-
Health check endpoint.
|
|
832
|
-
Returns server status and basic information.
|
|
833
|
-
"""
|
|
834
|
-
return {"status": "ok", "model": "mcp-proxy-adapter", "version": "1.0.0"}
|
|
835
|
-
|
|
836
|
-
# Graceful shutdown endpoint
|
|
837
|
-
@app.post("/shutdown")
|
|
838
|
-
async def shutdown_endpoint():
|
|
839
|
-
"""
|
|
840
|
-
Graceful shutdown endpoint.
|
|
841
|
-
Triggers server shutdown after completing current requests.
|
|
842
|
-
"""
|
|
843
|
-
import asyncio
|
|
844
|
-
|
|
845
|
-
# Schedule shutdown after a short delay to allow response
|
|
846
|
-
async def delayed_shutdown():
|
|
847
|
-
await asyncio.sleep(1)
|
|
848
|
-
# This will trigger the lifespan shutdown event
|
|
849
|
-
import os
|
|
850
|
-
|
|
851
|
-
os._exit(0)
|
|
852
|
-
|
|
853
|
-
# Start shutdown task
|
|
854
|
-
asyncio.create_task(delayed_shutdown())
|
|
855
|
-
|
|
856
|
-
return {
|
|
857
|
-
"status": "shutting_down",
|
|
858
|
-
"message": "Server shutdown initiated. New requests will be rejected.",
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
# List of available commands
|
|
862
|
-
@app.get("/api/commands", response_model=CommandListResponse)
|
|
863
|
-
async def commands_list_endpoint():
|
|
864
|
-
"""
|
|
865
|
-
Endpoint for getting list of available commands.
|
|
866
|
-
"""
|
|
867
|
-
commands = await get_commands_list()
|
|
868
|
-
return {"commands": commands}
|
|
869
|
-
|
|
870
|
-
# Get command information by name
|
|
871
|
-
@app.get("/api/commands/{command_name}")
|
|
872
|
-
async def command_info_endpoint(request: Request, command_name: str):
|
|
873
|
-
"""
|
|
874
|
-
Endpoint for getting information about a specific command.
|
|
875
|
-
"""
|
|
876
|
-
# Get request_id from middleware state
|
|
877
|
-
request_id = getattr(request.state, "request_id", None)
|
|
878
|
-
|
|
879
|
-
# Create request get_global_logger() for this endpoint
|
|
880
|
-
req_logger = RequestLogger(__name__, request_id) if request_id else get_global_logger()
|
|
881
|
-
|
|
882
|
-
try:
|
|
883
|
-
command_info = registry.get_command_info(command_name)
|
|
884
|
-
return command_info
|
|
885
|
-
except NotFoundError as e:
|
|
886
|
-
req_logger.warning(f"Command '{command_name}' not found")
|
|
887
|
-
return JSONResponse(
|
|
888
|
-
status_code=404,
|
|
889
|
-
content={
|
|
890
|
-
"error": {
|
|
891
|
-
"code": 404,
|
|
892
|
-
"message": f"Command '{command_name}' not found",
|
|
893
|
-
}
|
|
894
|
-
},
|
|
895
|
-
)
|
|
896
|
-
|
|
897
|
-
# Get API tool description
|
|
898
|
-
@app.get("/api/tools/{tool_name}")
|
|
899
|
-
async def tool_description_endpoint(tool_name: str, format: Optional[str] = "json"):
|
|
900
|
-
"""
|
|
901
|
-
Получить подробное описание инструмента API.
|
|
902
|
-
|
|
903
|
-
Возвращает полное описание инструмента API с доступными командами,
|
|
904
|
-
их параметрами и примерами использования. Формат возвращаемых данных
|
|
905
|
-
может быть JSON или Markdown (text).
|
|
906
|
-
|
|
907
|
-
Args:
|
|
908
|
-
tool_name: Имя инструмента API
|
|
909
|
-
format: Формат вывода (json, text, markdown, html)
|
|
910
|
-
"""
|
|
911
|
-
try:
|
|
912
|
-
description = get_tool_description(tool_name, format)
|
|
913
|
-
|
|
914
|
-
if format.lower() in ["text", "markdown", "html"]:
|
|
915
|
-
if format.lower() == "html":
|
|
916
|
-
return Response(content=description, media_type="text/html")
|
|
917
|
-
else:
|
|
918
|
-
return JSONResponse(
|
|
919
|
-
content={"description": description},
|
|
920
|
-
media_type="application/json",
|
|
921
|
-
)
|
|
922
|
-
else:
|
|
923
|
-
return description
|
|
924
|
-
|
|
925
|
-
except NotFoundError as e:
|
|
926
|
-
get_global_logger().warning(f"Tool not found: {tool_name}")
|
|
927
|
-
return JSONResponse(
|
|
928
|
-
status_code=404, content={"error": {"code": 404, "message": str(e)}}
|
|
929
|
-
)
|
|
930
|
-
except Exception as e:
|
|
931
|
-
get_global_logger().exception(f"Error generating tool description: {e}")
|
|
932
|
-
return JSONResponse(
|
|
933
|
-
status_code=500,
|
|
934
|
-
content={
|
|
935
|
-
"error": {
|
|
936
|
-
"code": 500,
|
|
937
|
-
"message": f"Error generating tool description: {str(e)}",
|
|
938
|
-
}
|
|
939
|
-
},
|
|
940
|
-
)
|
|
941
|
-
|
|
942
|
-
# Execute API tool
|
|
943
|
-
@app.post("/api/tools/{tool_name}")
|
|
944
|
-
async def execute_tool_endpoint(tool_name: str, params: Dict[str, Any] = Body(...)):
|
|
945
|
-
"""
|
|
946
|
-
Выполнить инструмент API с указанными параметрами.
|
|
947
|
-
|
|
948
|
-
Args:
|
|
949
|
-
tool_name: Имя инструмента API
|
|
950
|
-
params: Параметры инструмента
|
|
951
|
-
"""
|
|
952
|
-
try:
|
|
953
|
-
result = await execute_tool(tool_name, **params)
|
|
954
|
-
return result
|
|
955
|
-
except NotFoundError as e:
|
|
956
|
-
get_global_logger().warning(f"Tool not found: {tool_name}")
|
|
957
|
-
return JSONResponse(
|
|
958
|
-
status_code=404, content={"error": {"code": 404, "message": str(e)}}
|
|
959
|
-
)
|
|
960
|
-
except Exception as e:
|
|
961
|
-
get_global_logger().exception(f"Error executing tool {tool_name}: {e}")
|
|
962
|
-
return JSONResponse(
|
|
963
|
-
status_code=500,
|
|
964
|
-
content={
|
|
965
|
-
"error": {"code": 500, "message": f"Error executing tool: {str(e)}"}
|
|
966
|
-
},
|
|
967
|
-
)
|
|
968
|
-
|
|
969
|
-
return app
|
|
65
|
+
app_factory = AppFactory()
|
|
66
|
+
return app_factory.create_app(title, description, version, app_config, config_path)
|