mcp-proxy-adapter 6.3.3__py3-none-any.whl → 6.3.5__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/__init__.py +9 -5
- mcp_proxy_adapter/__main__.py +1 -1
- mcp_proxy_adapter/api/app.py +227 -176
- mcp_proxy_adapter/api/handlers.py +68 -60
- mcp_proxy_adapter/api/middleware/__init__.py +7 -5
- mcp_proxy_adapter/api/middleware/base.py +19 -16
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +44 -34
- mcp_proxy_adapter/api/middleware/error_handling.py +57 -67
- mcp_proxy_adapter/api/middleware/factory.py +50 -52
- mcp_proxy_adapter/api/middleware/logging.py +46 -30
- mcp_proxy_adapter/api/middleware/performance.py +19 -16
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +80 -50
- mcp_proxy_adapter/api/middleware/transport_middleware.py +26 -24
- mcp_proxy_adapter/api/middleware/unified_security.py +70 -51
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +43 -34
- mcp_proxy_adapter/api/schemas.py +69 -43
- mcp_proxy_adapter/api/tool_integration.py +83 -63
- mcp_proxy_adapter/api/tools.py +60 -50
- mcp_proxy_adapter/commands/__init__.py +15 -6
- mcp_proxy_adapter/commands/auth_validation_command.py +107 -110
- mcp_proxy_adapter/commands/base.py +108 -112
- mcp_proxy_adapter/commands/builtin_commands.py +28 -18
- mcp_proxy_adapter/commands/catalog_manager.py +394 -265
- mcp_proxy_adapter/commands/cert_monitor_command.py +222 -204
- mcp_proxy_adapter/commands/certificate_management_command.py +210 -213
- mcp_proxy_adapter/commands/command_registry.py +275 -226
- mcp_proxy_adapter/commands/config_command.py +48 -33
- mcp_proxy_adapter/commands/dependency_container.py +22 -23
- mcp_proxy_adapter/commands/dependency_manager.py +65 -56
- mcp_proxy_adapter/commands/echo_command.py +15 -15
- mcp_proxy_adapter/commands/health_command.py +31 -29
- mcp_proxy_adapter/commands/help_command.py +97 -61
- mcp_proxy_adapter/commands/hooks.py +65 -49
- mcp_proxy_adapter/commands/key_management_command.py +148 -147
- mcp_proxy_adapter/commands/load_command.py +58 -40
- mcp_proxy_adapter/commands/plugins_command.py +80 -54
- mcp_proxy_adapter/commands/protocol_management_command.py +60 -48
- mcp_proxy_adapter/commands/proxy_registration_command.py +107 -115
- mcp_proxy_adapter/commands/reload_command.py +43 -37
- mcp_proxy_adapter/commands/result.py +26 -33
- mcp_proxy_adapter/commands/role_test_command.py +26 -26
- mcp_proxy_adapter/commands/roles_management_command.py +176 -173
- mcp_proxy_adapter/commands/security_command.py +134 -122
- mcp_proxy_adapter/commands/settings_command.py +47 -56
- mcp_proxy_adapter/commands/ssl_setup_command.py +109 -129
- mcp_proxy_adapter/commands/token_management_command.py +129 -158
- mcp_proxy_adapter/commands/transport_management_command.py +41 -36
- mcp_proxy_adapter/commands/unload_command.py +42 -37
- mcp_proxy_adapter/config.py +36 -35
- mcp_proxy_adapter/core/__init__.py +19 -21
- mcp_proxy_adapter/core/app_factory.py +30 -9
- mcp_proxy_adapter/core/app_runner.py +81 -64
- mcp_proxy_adapter/core/auth_validator.py +176 -182
- mcp_proxy_adapter/core/certificate_utils.py +469 -426
- mcp_proxy_adapter/core/client.py +155 -126
- mcp_proxy_adapter/core/client_manager.py +60 -54
- mcp_proxy_adapter/core/client_security.py +108 -88
- mcp_proxy_adapter/core/config_converter.py +176 -143
- mcp_proxy_adapter/core/config_validator.py +12 -4
- mcp_proxy_adapter/core/crl_utils.py +21 -7
- mcp_proxy_adapter/core/errors.py +64 -20
- mcp_proxy_adapter/core/logging.py +34 -29
- mcp_proxy_adapter/core/mtls_asgi.py +29 -25
- mcp_proxy_adapter/core/mtls_asgi_app.py +66 -54
- mcp_proxy_adapter/core/protocol_manager.py +154 -104
- mcp_proxy_adapter/core/proxy_client.py +202 -144
- mcp_proxy_adapter/core/proxy_registration.py +12 -2
- mcp_proxy_adapter/core/role_utils.py +139 -125
- mcp_proxy_adapter/core/security_adapter.py +88 -77
- mcp_proxy_adapter/core/security_factory.py +50 -44
- mcp_proxy_adapter/core/security_integration.py +72 -24
- mcp_proxy_adapter/core/server_adapter.py +68 -64
- mcp_proxy_adapter/core/server_engine.py +71 -53
- mcp_proxy_adapter/core/settings.py +68 -58
- mcp_proxy_adapter/core/ssl_utils.py +69 -56
- mcp_proxy_adapter/core/transport_manager.py +72 -60
- mcp_proxy_adapter/core/unified_config_adapter.py +201 -150
- mcp_proxy_adapter/core/utils.py +4 -2
- mcp_proxy_adapter/custom_openapi.py +107 -99
- mcp_proxy_adapter/examples/basic_framework/main.py +9 -2
- mcp_proxy_adapter/examples/commands/__init__.py +1 -1
- mcp_proxy_adapter/examples/create_certificates_simple.py +182 -71
- mcp_proxy_adapter/examples/debug_request_state.py +38 -19
- mcp_proxy_adapter/examples/debug_role_chain.py +53 -20
- mcp_proxy_adapter/examples/demo_client.py +48 -36
- mcp_proxy_adapter/examples/examples/basic_framework/main.py +9 -2
- mcp_proxy_adapter/examples/examples/full_application/__init__.py +1 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +22 -10
- mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +24 -17
- mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +16 -3
- mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +13 -3
- mcp_proxy_adapter/examples/examples/full_application/main.py +27 -2
- mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +48 -14
- mcp_proxy_adapter/examples/full_application/__init__.py +1 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +22 -10
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +24 -17
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +16 -3
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +13 -3
- mcp_proxy_adapter/examples/full_application/main.py +27 -2
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +48 -14
- mcp_proxy_adapter/examples/generate_all_certificates.py +198 -73
- mcp_proxy_adapter/examples/generate_certificates.py +31 -16
- mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +220 -74
- mcp_proxy_adapter/examples/generate_test_configs.py +68 -91
- mcp_proxy_adapter/examples/proxy_registration_example.py +76 -75
- mcp_proxy_adapter/examples/run_example.py +23 -5
- mcp_proxy_adapter/examples/run_full_test_suite.py +109 -71
- mcp_proxy_adapter/examples/run_proxy_server.py +22 -9
- mcp_proxy_adapter/examples/run_security_tests.py +103 -41
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +72 -36
- mcp_proxy_adapter/examples/scripts/config_generator.py +288 -187
- mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +185 -72
- mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +220 -74
- mcp_proxy_adapter/examples/security_test_client.py +196 -127
- mcp_proxy_adapter/examples/setup_test_environment.py +17 -29
- mcp_proxy_adapter/examples/test_config.py +19 -4
- mcp_proxy_adapter/examples/test_config_generator.py +23 -7
- mcp_proxy_adapter/examples/test_examples.py +84 -56
- mcp_proxy_adapter/examples/universal_client.py +119 -62
- mcp_proxy_adapter/openapi.py +108 -115
- mcp_proxy_adapter/utils/config_generator.py +429 -274
- mcp_proxy_adapter/version.py +1 -2
- {mcp_proxy_adapter-6.3.3.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/METADATA +1 -1
- mcp_proxy_adapter-6.3.5.dist-info/RECORD +143 -0
- mcp_proxy_adapter-6.3.3.dist-info/RECORD +0 -143
- {mcp_proxy_adapter-6.3.3.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.3.3.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/entry_points.txt +0 -0
- {mcp_proxy_adapter-6.3.3.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-6.3.3.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/top_level.txt +0 -0
mcp_proxy_adapter/api/app.py
CHANGED
@@ -13,9 +13,22 @@ from fastapi import FastAPI, Body, Depends, HTTPException, Request
|
|
13
13
|
from fastapi.responses import JSONResponse, Response
|
14
14
|
from fastapi.middleware.cors import CORSMiddleware
|
15
15
|
|
16
|
-
from mcp_proxy_adapter.api.handlers import
|
16
|
+
from mcp_proxy_adapter.api.handlers import (
|
17
|
+
execute_command,
|
18
|
+
handle_json_rpc,
|
19
|
+
handle_batch_json_rpc,
|
20
|
+
get_server_health,
|
21
|
+
get_commands_list,
|
22
|
+
)
|
17
23
|
from mcp_proxy_adapter.api.middleware import setup_middleware
|
18
|
-
from mcp_proxy_adapter.api.schemas import
|
24
|
+
from mcp_proxy_adapter.api.schemas import (
|
25
|
+
JsonRpcRequest,
|
26
|
+
JsonRpcSuccessResponse,
|
27
|
+
JsonRpcErrorResponse,
|
28
|
+
HealthResponse,
|
29
|
+
CommandListResponse,
|
30
|
+
APIToolDescription,
|
31
|
+
)
|
19
32
|
from mcp_proxy_adapter.api.tools import get_tool_description, execute_tool
|
20
33
|
from mcp_proxy_adapter.config import config
|
21
34
|
from mcp_proxy_adapter.core.errors import MicroserviceError, NotFoundError
|
@@ -28,13 +41,14 @@ from mcp_proxy_adapter.custom_openapi import custom_openapi_with_fallback
|
|
28
41
|
def create_lifespan(config_path: Optional[str] = None):
|
29
42
|
"""
|
30
43
|
Create lifespan manager for the FastAPI application.
|
31
|
-
|
44
|
+
|
32
45
|
Args:
|
33
46
|
config_path: Path to configuration file (optional)
|
34
|
-
|
47
|
+
|
35
48
|
Returns:
|
36
49
|
Lifespan context manager
|
37
50
|
"""
|
51
|
+
|
38
52
|
@asynccontextmanager
|
39
53
|
async def lifespan(app: FastAPI):
|
40
54
|
"""
|
@@ -47,7 +61,7 @@ def create_lifespan(config_path: Optional[str] = None):
|
|
47
61
|
unregister_from_proxy,
|
48
62
|
initialize_proxy_registration,
|
49
63
|
)
|
50
|
-
|
64
|
+
|
51
65
|
# Initialize proxy registration manager WITH CURRENT CONFIG before reload_system
|
52
66
|
# so that registration inside reload_system can work
|
53
67
|
try:
|
@@ -62,10 +76,12 @@ def create_lifespan(config_path: Optional[str] = None):
|
|
62
76
|
init_result = await registry.reload_system(config_path=config_path)
|
63
77
|
else:
|
64
78
|
init_result = await registry.reload_system()
|
65
|
-
|
66
|
-
logger.info(
|
79
|
+
|
80
|
+
logger.info(
|
81
|
+
f"Application started with {init_result['total_commands']} commands registered"
|
82
|
+
)
|
67
83
|
logger.info(f"System initialization result: {init_result}")
|
68
|
-
|
84
|
+
|
69
85
|
# Initialize proxy registration manager with current config
|
70
86
|
try:
|
71
87
|
initialize_proxy_registration(config.get_all())
|
@@ -76,27 +92,27 @@ def create_lifespan(config_path: Optional[str] = None):
|
|
76
92
|
server_config = config.get("server", {})
|
77
93
|
server_host = server_config.get("host", "0.0.0.0")
|
78
94
|
server_port = server_config.get("port", 8000)
|
79
|
-
|
95
|
+
|
80
96
|
# Determine server URL based on SSL configuration
|
81
97
|
# Try security framework SSL config first
|
82
98
|
security_config = config.get("security", {})
|
83
99
|
ssl_config = security_config.get("ssl", {})
|
84
|
-
|
100
|
+
|
85
101
|
# Fallback to legacy SSL config
|
86
102
|
if not ssl_config.get("enabled", False):
|
87
103
|
ssl_config = config.get("ssl", {})
|
88
|
-
|
104
|
+
|
89
105
|
if ssl_config.get("enabled", False):
|
90
106
|
protocol = "https"
|
91
107
|
else:
|
92
108
|
protocol = "http"
|
93
|
-
|
109
|
+
|
94
110
|
# Use localhost for external access if host is 0.0.0.0
|
95
111
|
if server_host == "0.0.0.0":
|
96
112
|
server_host = "localhost"
|
97
|
-
|
113
|
+
|
98
114
|
server_url = f"{protocol}://{server_host}:{server_port}"
|
99
|
-
|
115
|
+
|
100
116
|
# Attempt proxy registration in background with small delay
|
101
117
|
async def _delayed_register():
|
102
118
|
try:
|
@@ -110,53 +126,55 @@ def create_lifespan(config_path: Optional[str] = None):
|
|
110
126
|
logger.error(f"Proxy registration failed: {e}")
|
111
127
|
|
112
128
|
asyncio.create_task(_delayed_register())
|
113
|
-
|
129
|
+
|
114
130
|
yield # Application is running
|
115
|
-
|
131
|
+
|
116
132
|
# Shutdown events
|
117
133
|
logger.info("Application shutting down")
|
118
|
-
|
134
|
+
|
119
135
|
# Unregister from proxy if enabled
|
120
136
|
unregistration_success = await unregister_from_proxy()
|
121
137
|
if unregistration_success:
|
122
138
|
logger.info("✅ Proxy unregistration completed successfully")
|
123
139
|
else:
|
124
140
|
logger.warning("⚠️ Proxy unregistration failed or was disabled")
|
125
|
-
|
141
|
+
|
126
142
|
return lifespan
|
127
143
|
|
128
144
|
|
129
|
-
def create_ssl_context(
|
145
|
+
def create_ssl_context(
|
146
|
+
app_config: Optional[Dict[str, Any]] = None
|
147
|
+
) -> Optional[ssl.SSLContext]:
|
130
148
|
"""
|
131
149
|
Create SSL context based on configuration.
|
132
|
-
|
150
|
+
|
133
151
|
Args:
|
134
152
|
app_config: Application configuration dictionary (optional)
|
135
|
-
|
153
|
+
|
136
154
|
Returns:
|
137
155
|
SSL context if SSL is enabled and properly configured, None otherwise
|
138
156
|
"""
|
139
157
|
current_config = app_config if app_config is not None else config.get_all()
|
140
|
-
|
158
|
+
|
141
159
|
# Try security framework SSL config first
|
142
160
|
security_config = current_config.get("security", {})
|
143
161
|
ssl_config = security_config.get("ssl", {})
|
144
|
-
|
162
|
+
|
145
163
|
# Fallback to legacy SSL config
|
146
164
|
if not ssl_config.get("enabled", False):
|
147
165
|
ssl_config = current_config.get("ssl", {})
|
148
|
-
|
166
|
+
|
149
167
|
if not ssl_config.get("enabled", False):
|
150
168
|
logger.info("SSL is disabled in configuration")
|
151
169
|
return None
|
152
|
-
|
170
|
+
|
153
171
|
cert_file = ssl_config.get("cert_file")
|
154
172
|
key_file = ssl_config.get("key_file")
|
155
|
-
|
173
|
+
|
156
174
|
if not cert_file or not key_file:
|
157
175
|
logger.warning("SSL enabled but certificate or key file not specified")
|
158
176
|
return None
|
159
|
-
|
177
|
+
|
160
178
|
try:
|
161
179
|
# Create SSL context using SSLUtils
|
162
180
|
ssl_context = SSLUtils.create_ssl_context(
|
@@ -166,18 +184,26 @@ def create_ssl_context(app_config: Optional[Dict[str, Any]] = None) -> Optional[
|
|
166
184
|
verify_client=ssl_config.get("verify_client", False),
|
167
185
|
cipher_suites=ssl_config.get("cipher_suites", []),
|
168
186
|
min_tls_version=ssl_config.get("min_tls_version", "1.2"),
|
169
|
-
max_tls_version=ssl_config.get("max_tls_version", "1.3")
|
187
|
+
max_tls_version=ssl_config.get("max_tls_version", "1.3"),
|
188
|
+
)
|
189
|
+
|
190
|
+
logger.info(
|
191
|
+
f"SSL context created successfully for mode: {ssl_config.get('mode', 'https_only')}"
|
170
192
|
)
|
171
|
-
|
172
|
-
logger.info(f"SSL context created successfully for mode: {ssl_config.get('mode', 'https_only')}")
|
173
193
|
return ssl_context
|
174
|
-
|
194
|
+
|
175
195
|
except Exception as e:
|
176
196
|
logger.error(f"Failed to create SSL context: {e}")
|
177
197
|
return None
|
178
198
|
|
179
199
|
|
180
|
-
def create_app(
|
200
|
+
def create_app(
|
201
|
+
title: Optional[str] = None,
|
202
|
+
description: Optional[str] = None,
|
203
|
+
version: Optional[str] = None,
|
204
|
+
app_config: Optional[Dict[str, Any]] = None,
|
205
|
+
config_path: Optional[str] = None,
|
206
|
+
) -> FastAPI:
|
181
207
|
"""
|
182
208
|
Creates and configures FastAPI application.
|
183
209
|
|
@@ -190,76 +216,97 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
190
216
|
|
191
217
|
Returns:
|
192
218
|
Configured FastAPI application.
|
193
|
-
|
219
|
+
|
194
220
|
Raises:
|
195
221
|
SystemExit: If authentication is enabled but required files are missing (security issue)
|
196
222
|
"""
|
197
223
|
# Use provided configuration or fallback to global config
|
198
224
|
if app_config is not None:
|
199
|
-
if hasattr(app_config,
|
225
|
+
if hasattr(app_config, "get_all"):
|
200
226
|
current_config = app_config.get_all()
|
201
|
-
elif hasattr(app_config,
|
227
|
+
elif hasattr(app_config, "keys"):
|
202
228
|
current_config = app_config
|
203
229
|
else:
|
204
230
|
current_config = config.get_all()
|
205
231
|
else:
|
206
232
|
current_config = config.get_all()
|
207
|
-
|
233
|
+
|
208
234
|
# Debug: Check what config is passed to create_app
|
209
235
|
if app_config:
|
210
|
-
if hasattr(app_config,
|
211
|
-
print(
|
236
|
+
if hasattr(app_config, "keys"):
|
237
|
+
print(
|
238
|
+
f"🔍 Debug: create_app received app_config keys: {list(app_config.keys())}"
|
239
|
+
)
|
212
240
|
if "security" in app_config:
|
213
241
|
ssl_config = app_config["security"].get("ssl", {})
|
214
|
-
print(
|
215
|
-
|
216
|
-
|
242
|
+
print(
|
243
|
+
f"🔍 Debug: create_app SSL config: enabled={ssl_config.get('enabled', False)}"
|
244
|
+
)
|
245
|
+
print(
|
246
|
+
f"🔍 Debug: create_app SSL config: cert_file={ssl_config.get('cert_file')}"
|
247
|
+
)
|
248
|
+
print(
|
249
|
+
f"🔍 Debug: create_app SSL config: key_file={ssl_config.get('key_file')}"
|
250
|
+
)
|
217
251
|
else:
|
218
252
|
print(f"🔍 Debug: create_app received app_config type: {type(app_config)}")
|
219
253
|
else:
|
220
254
|
print("🔍 Debug: create_app received no app_config, using global config")
|
221
|
-
|
255
|
+
|
222
256
|
# Security check: Validate all authentication configurations before startup
|
223
257
|
security_errors = []
|
224
|
-
|
258
|
+
|
225
259
|
print(f"🔍 Debug: current_config keys: {list(current_config.keys())}")
|
226
260
|
if "security" in current_config:
|
227
261
|
print(f"🔍 Debug: security config: {current_config['security']}")
|
228
262
|
if "roles" in current_config:
|
229
263
|
print(f"🔍 Debug: roles config: {current_config['roles']}")
|
230
|
-
|
264
|
+
|
231
265
|
# Check security framework configuration only if enabled
|
232
266
|
security_config = current_config.get("security", {})
|
233
267
|
if security_config.get("enabled", False):
|
234
268
|
# Validate security framework configuration
|
235
269
|
from mcp_proxy_adapter.core.unified_config_adapter import UnifiedConfigAdapter
|
270
|
+
|
236
271
|
adapter = UnifiedConfigAdapter()
|
237
272
|
validation_result = adapter.validate_configuration(current_config)
|
238
|
-
|
273
|
+
|
239
274
|
if not validation_result.is_valid:
|
240
275
|
security_errors.extend(validation_result.errors)
|
241
|
-
|
276
|
+
|
242
277
|
# Check SSL configuration within security framework
|
243
278
|
ssl_config = security_config.get("ssl", {})
|
244
279
|
if ssl_config.get("enabled", False):
|
245
280
|
cert_file = ssl_config.get("cert_file")
|
246
281
|
key_file = ssl_config.get("key_file")
|
247
|
-
|
248
|
-
print(
|
249
|
-
|
250
|
-
|
251
|
-
|
282
|
+
|
283
|
+
print(
|
284
|
+
f"🔍 Debug: api/app.py security.ssl: cert_file={cert_file}, key_file={key_file}"
|
285
|
+
)
|
286
|
+
print(
|
287
|
+
f"🔍 Debug: api/app.py security.ssl: cert_file exists={Path(cert_file).exists() if cert_file else 'None'}"
|
288
|
+
)
|
289
|
+
print(
|
290
|
+
f"🔍 Debug: api/app.py security.ssl: key_file exists={Path(key_file).exists() if key_file else 'None'}"
|
291
|
+
)
|
292
|
+
|
252
293
|
if cert_file and not Path(cert_file).exists():
|
253
|
-
security_errors.append(
|
254
|
-
|
294
|
+
security_errors.append(
|
295
|
+
f"SSL is enabled but certificate file not found: {cert_file}"
|
296
|
+
)
|
297
|
+
|
255
298
|
if key_file and not Path(key_file).exists():
|
256
|
-
security_errors.append(
|
257
|
-
|
299
|
+
security_errors.append(
|
300
|
+
f"SSL is enabled but private key file not found: {key_file}"
|
301
|
+
)
|
302
|
+
|
258
303
|
# Check mTLS configuration
|
259
304
|
ca_cert_file = ssl_config.get("ca_cert_file")
|
260
305
|
if ca_cert_file and not Path(ca_cert_file).exists():
|
261
|
-
security_errors.append(
|
262
|
-
|
306
|
+
security_errors.append(
|
307
|
+
f"mTLS is enabled but CA certificate file not found: {ca_cert_file}"
|
308
|
+
)
|
309
|
+
|
263
310
|
# Legacy configuration checks for backward compatibility
|
264
311
|
roles_config = current_config.get("roles", {})
|
265
312
|
print(f"🔍 Debug: roles_config = {roles_config}")
|
@@ -267,69 +314,93 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
267
314
|
roles_config_path = roles_config.get("config_file", "schemas/roles_schema.json")
|
268
315
|
print(f"🔍 Debug: Checking roles file: {roles_config_path}")
|
269
316
|
if not Path(roles_config_path).exists():
|
270
|
-
security_errors.append(
|
271
|
-
|
317
|
+
security_errors.append(
|
318
|
+
f"Roles are enabled but schema file not found: {roles_config_path}"
|
319
|
+
)
|
320
|
+
|
272
321
|
# Check new security framework permissions configuration
|
273
322
|
security_config = current_config.get("security", {})
|
274
323
|
permissions_config = security_config.get("permissions", {})
|
275
324
|
if permissions_config.get("enabled", False):
|
276
325
|
roles_file = permissions_config.get("roles_file")
|
277
326
|
if roles_file and not Path(roles_file).exists():
|
278
|
-
security_errors.append(
|
279
|
-
|
327
|
+
security_errors.append(
|
328
|
+
f"Permissions are enabled but roles file not found: {roles_file}"
|
329
|
+
)
|
330
|
+
|
280
331
|
legacy_ssl_config = current_config.get("ssl", {})
|
281
332
|
if legacy_ssl_config.get("enabled", False):
|
282
333
|
# Check SSL certificate files
|
283
334
|
cert_file = legacy_ssl_config.get("cert_file")
|
284
335
|
key_file = legacy_ssl_config.get("key_file")
|
285
|
-
|
286
|
-
print(
|
287
|
-
|
288
|
-
|
289
|
-
|
336
|
+
|
337
|
+
print(
|
338
|
+
f"🔍 Debug: api/app.py legacy.ssl: cert_file={cert_file}, key_file={key_file}"
|
339
|
+
)
|
340
|
+
print(
|
341
|
+
f"🔍 Debug: api/app.py legacy.ssl: cert_file exists={Path(cert_file).exists() if cert_file else 'None'}"
|
342
|
+
)
|
343
|
+
print(
|
344
|
+
f"🔍 Debug: api/app.py legacy.ssl: key_file exists={Path(key_file).exists() if key_file else 'None'}"
|
345
|
+
)
|
346
|
+
|
290
347
|
if cert_file and not Path(cert_file).exists():
|
291
|
-
security_errors.append(
|
292
|
-
|
348
|
+
security_errors.append(
|
349
|
+
f"Legacy SSL is enabled but certificate file not found: {cert_file}"
|
350
|
+
)
|
351
|
+
|
293
352
|
if key_file and not Path(key_file).exists():
|
294
|
-
security_errors.append(
|
295
|
-
|
353
|
+
security_errors.append(
|
354
|
+
f"Legacy SSL is enabled but private key file not found: {key_file}"
|
355
|
+
)
|
356
|
+
|
296
357
|
# Check mTLS configuration
|
297
358
|
if legacy_ssl_config.get("mode") == "mtls":
|
298
359
|
ca_cert = legacy_ssl_config.get("ca_cert")
|
299
360
|
if ca_cert and not Path(ca_cert).exists():
|
300
|
-
security_errors.append(
|
301
|
-
|
361
|
+
security_errors.append(
|
362
|
+
f"Legacy mTLS is enabled but CA certificate file not found: {ca_cert}"
|
363
|
+
)
|
364
|
+
|
302
365
|
# Check token authentication configuration
|
303
366
|
token_auth_config = legacy_ssl_config.get("token_auth", {})
|
304
367
|
if token_auth_config.get("enabled", False):
|
305
368
|
tokens_file = token_auth_config.get("tokens_file", "tokens.json")
|
306
369
|
if not Path(tokens_file).exists():
|
307
|
-
security_errors.append(
|
308
|
-
|
370
|
+
security_errors.append(
|
371
|
+
f"Token authentication is enabled but tokens file not found: {tokens_file}"
|
372
|
+
)
|
373
|
+
|
309
374
|
# Check general authentication
|
310
375
|
if current_config.get("auth_enabled", False):
|
311
376
|
# If auth is enabled, check if any authentication method is properly configured
|
312
377
|
ssl_enabled = legacy_ssl_config.get("enabled", False)
|
313
378
|
roles_enabled = roles_config.get("enabled", False)
|
314
379
|
token_auth_enabled = token_auth_config.get("enabled", False)
|
315
|
-
|
380
|
+
|
316
381
|
if not (ssl_enabled or roles_enabled or token_auth_enabled):
|
317
|
-
security_errors.append(
|
318
|
-
|
382
|
+
security_errors.append(
|
383
|
+
"Authentication is enabled but no authentication method is properly configured"
|
384
|
+
)
|
385
|
+
|
319
386
|
# If there are security errors, block startup
|
320
387
|
if security_errors:
|
321
|
-
logger.critical(
|
388
|
+
logger.critical(
|
389
|
+
"CRITICAL SECURITY ERROR: Authentication configuration issues detected:"
|
390
|
+
)
|
322
391
|
for error in security_errors:
|
323
392
|
logger.critical(f" - {error}")
|
324
393
|
logger.critical("Server startup blocked for security reasons.")
|
325
|
-
logger.critical(
|
394
|
+
logger.critical(
|
395
|
+
"Please fix authentication configuration or disable authentication features."
|
396
|
+
)
|
326
397
|
raise SystemExit(1)
|
327
|
-
|
398
|
+
|
328
399
|
# Use provided parameters or defaults
|
329
400
|
app_title = title or "MCP Proxy Adapter"
|
330
401
|
app_description = description or "JSON-RPC API for interacting with MCP Proxy"
|
331
402
|
app_version = version or "1.0.0"
|
332
|
-
|
403
|
+
|
333
404
|
# Create application
|
334
405
|
app = FastAPI(
|
335
406
|
title=app_title,
|
@@ -339,7 +410,7 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
339
410
|
redoc_url="/redoc",
|
340
411
|
lifespan=create_lifespan(config_path),
|
341
412
|
)
|
342
|
-
|
413
|
+
|
343
414
|
# Configure CORS
|
344
415
|
app.add_middleware(
|
345
416
|
CORSMiddleware,
|
@@ -348,13 +419,13 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
348
419
|
allow_methods=["*"],
|
349
420
|
allow_headers=["*"],
|
350
421
|
)
|
351
|
-
|
422
|
+
|
352
423
|
# Setup middleware using the new middleware package
|
353
424
|
setup_middleware(app, current_config)
|
354
|
-
|
425
|
+
|
355
426
|
# Use custom OpenAPI schema
|
356
427
|
app.openapi = lambda: custom_openapi_with_fallback(app)
|
357
|
-
|
428
|
+
|
358
429
|
# Explicit endpoint for OpenAPI schema
|
359
430
|
@app.get("/openapi.json")
|
360
431
|
async def get_openapi_schema():
|
@@ -364,18 +435,28 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
364
435
|
return custom_openapi_with_fallback(app)
|
365
436
|
|
366
437
|
# JSON-RPC handler
|
367
|
-
@app.post(
|
368
|
-
|
438
|
+
@app.post(
|
439
|
+
"/api/jsonrpc",
|
440
|
+
response_model=Union[
|
441
|
+
JsonRpcSuccessResponse,
|
442
|
+
JsonRpcErrorResponse,
|
443
|
+
List[Union[JsonRpcSuccessResponse, JsonRpcErrorResponse]],
|
444
|
+
],
|
445
|
+
)
|
446
|
+
async def jsonrpc_endpoint(
|
447
|
+
request: Request,
|
448
|
+
request_data: Union[Dict[str, Any], List[Dict[str, Any]]] = Body(...),
|
449
|
+
):
|
369
450
|
"""
|
370
451
|
Endpoint for handling JSON-RPC requests.
|
371
452
|
Supports both single and batch requests.
|
372
453
|
"""
|
373
454
|
# Get request_id from middleware state
|
374
455
|
request_id = getattr(request.state, "request_id", None)
|
375
|
-
|
456
|
+
|
376
457
|
# Create request logger for this endpoint
|
377
458
|
req_logger = RequestLogger(__name__, request_id) if request_id else logger
|
378
|
-
|
459
|
+
|
379
460
|
# Check if it's a batch request
|
380
461
|
if isinstance(request_data, list):
|
381
462
|
# Process batch request
|
@@ -388,10 +469,10 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
388
469
|
"jsonrpc": "2.0",
|
389
470
|
"error": {
|
390
471
|
"code": -32600,
|
391
|
-
"message": "Invalid Request. Empty batch request"
|
472
|
+
"message": "Invalid Request. Empty batch request",
|
392
473
|
},
|
393
|
-
"id": None
|
394
|
-
}
|
474
|
+
"id": None,
|
475
|
+
},
|
395
476
|
)
|
396
477
|
return await handle_batch_json_rpc(request_data, request)
|
397
478
|
else:
|
@@ -411,7 +492,7 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
411
492
|
// Command parameters
|
412
493
|
}
|
413
494
|
}
|
414
|
-
|
495
|
+
|
415
496
|
2. JSON-RPC:
|
416
497
|
{
|
417
498
|
"jsonrpc": "2.0",
|
@@ -424,16 +505,16 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
424
505
|
"""
|
425
506
|
# Get request_id from middleware state
|
426
507
|
request_id = getattr(request.state, "request_id", None)
|
427
|
-
|
508
|
+
|
428
509
|
# Create request logger for this endpoint
|
429
510
|
req_logger = RequestLogger(__name__, request_id) if request_id else logger
|
430
|
-
|
511
|
+
|
431
512
|
try:
|
432
513
|
# Determine request format (CommandRequest or JSON-RPC)
|
433
514
|
if "jsonrpc" in command_data and "method" in command_data:
|
434
515
|
# JSON-RPC format
|
435
516
|
return await handle_json_rpc(command_data, request_id, request)
|
436
|
-
|
517
|
+
|
437
518
|
# CommandRequest format
|
438
519
|
if "command" not in command_data:
|
439
520
|
req_logger.warning("Missing required field 'command'")
|
@@ -442,16 +523,18 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
442
523
|
content={
|
443
524
|
"error": {
|
444
525
|
"code": -32600,
|
445
|
-
"message": "Отсутствует обязательное поле 'command'"
|
526
|
+
"message": "Отсутствует обязательное поле 'command'",
|
446
527
|
}
|
447
|
-
}
|
528
|
+
},
|
448
529
|
)
|
449
|
-
|
530
|
+
|
450
531
|
command_name = command_data["command"]
|
451
532
|
params = command_data.get("params", {})
|
452
|
-
|
453
|
-
req_logger.debug(
|
454
|
-
|
533
|
+
|
534
|
+
req_logger.debug(
|
535
|
+
f"Executing command via /cmd: {command_name}, params: {params}"
|
536
|
+
)
|
537
|
+
|
455
538
|
# Check if command exists
|
456
539
|
if not registry.command_exists(command_name):
|
457
540
|
req_logger.warning(f"Command '{command_name}' not found")
|
@@ -460,24 +543,21 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
460
543
|
content={
|
461
544
|
"error": {
|
462
545
|
"code": -32601,
|
463
|
-
"message": f"Команда '{command_name}' не найдена"
|
546
|
+
"message": f"Команда '{command_name}' не найдена",
|
464
547
|
}
|
465
|
-
}
|
548
|
+
},
|
466
549
|
)
|
467
|
-
|
550
|
+
|
468
551
|
# Execute command
|
469
552
|
try:
|
470
|
-
result = await execute_command(
|
553
|
+
result = await execute_command(
|
554
|
+
command_name, params, request_id, request
|
555
|
+
)
|
471
556
|
return {"result": result}
|
472
557
|
except MicroserviceError as e:
|
473
558
|
# Handle command execution errors
|
474
559
|
req_logger.error(f"Error executing command '{command_name}': {str(e)}")
|
475
|
-
return JSONResponse(
|
476
|
-
status_code=200,
|
477
|
-
content={
|
478
|
-
"error": e.to_dict()
|
479
|
-
}
|
480
|
-
)
|
560
|
+
return JSONResponse(status_code=200, content={"error": e.to_dict()})
|
481
561
|
except NotFoundError as e:
|
482
562
|
# Специальная обработка для help-команды: возвращаем result с пустым commands и error
|
483
563
|
if command_name == "help":
|
@@ -486,30 +566,20 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
486
566
|
"success": False,
|
487
567
|
"commands": {},
|
488
568
|
"error": str(e),
|
489
|
-
"note":
|
569
|
+
"note": 'To get detailed information about a specific command, call help with parameter: POST /cmd {"command": "help", "params": {"cmdname": "<command_name>"}}',
|
490
570
|
}
|
491
571
|
}
|
492
572
|
# Для остальных команд — стандартная ошибка
|
493
573
|
return JSONResponse(
|
494
574
|
status_code=200,
|
495
|
-
content={
|
496
|
-
"error": {
|
497
|
-
"code": e.code,
|
498
|
-
"message": str(e)
|
499
|
-
}
|
500
|
-
}
|
575
|
+
content={"error": {"code": e.code, "message": str(e)}},
|
501
576
|
)
|
502
|
-
|
577
|
+
|
503
578
|
except json.JSONDecodeError:
|
504
579
|
req_logger.error("JSON decode error")
|
505
580
|
return JSONResponse(
|
506
581
|
status_code=200,
|
507
|
-
content={
|
508
|
-
"error": {
|
509
|
-
"code": -32700,
|
510
|
-
"message": "Parse error"
|
511
|
-
}
|
512
|
-
}
|
582
|
+
content={"error": {"code": -32700, "message": "Parse error"}},
|
513
583
|
)
|
514
584
|
except Exception as e:
|
515
585
|
req_logger.exception(f"Unexpected error: {str(e)}")
|
@@ -519,30 +589,29 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
519
589
|
"error": {
|
520
590
|
"code": -32603,
|
521
591
|
"message": "Internal error",
|
522
|
-
"data": {"details": str(e)}
|
592
|
+
"data": {"details": str(e)},
|
523
593
|
}
|
524
|
-
}
|
594
|
+
},
|
525
595
|
)
|
526
596
|
|
527
597
|
# Direct command call
|
528
598
|
@app.post("/api/command/{command_name}")
|
529
|
-
async def command_endpoint(
|
599
|
+
async def command_endpoint(
|
600
|
+
request: Request, command_name: str, params: Dict[str, Any] = Body(default={})
|
601
|
+
):
|
530
602
|
"""
|
531
603
|
Endpoint for direct command call.
|
532
604
|
"""
|
533
605
|
# Get request_id from middleware state
|
534
606
|
request_id = getattr(request.state, "request_id", None)
|
535
|
-
|
607
|
+
|
536
608
|
try:
|
537
609
|
result = await execute_command(command_name, params, request_id, request)
|
538
610
|
return result
|
539
611
|
except MicroserviceError as e:
|
540
612
|
# Convert to proper HTTP status code
|
541
613
|
status_code = 400 if e.code < 0 else e.code
|
542
|
-
return JSONResponse(
|
543
|
-
status_code=status_code,
|
544
|
-
content=e.to_dict()
|
545
|
-
)
|
614
|
+
return JSONResponse(status_code=status_code, content=e.to_dict())
|
546
615
|
|
547
616
|
# Server health check
|
548
617
|
@app.get("/health", operation_id="health_check")
|
@@ -551,12 +620,8 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
551
620
|
Health check endpoint.
|
552
621
|
Returns server status and basic information.
|
553
622
|
"""
|
554
|
-
return {
|
555
|
-
|
556
|
-
"model": "mcp-proxy-adapter",
|
557
|
-
"version": "1.0.0"
|
558
|
-
}
|
559
|
-
|
623
|
+
return {"status": "ok", "model": "mcp-proxy-adapter", "version": "1.0.0"}
|
624
|
+
|
560
625
|
# Graceful shutdown endpoint
|
561
626
|
@app.post("/shutdown")
|
562
627
|
async def shutdown_endpoint():
|
@@ -565,20 +630,21 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
565
630
|
Triggers server shutdown after completing current requests.
|
566
631
|
"""
|
567
632
|
import asyncio
|
568
|
-
|
633
|
+
|
569
634
|
# Schedule shutdown after a short delay to allow response
|
570
635
|
async def delayed_shutdown():
|
571
636
|
await asyncio.sleep(1)
|
572
637
|
# This will trigger the lifespan shutdown event
|
573
638
|
import os
|
639
|
+
|
574
640
|
os._exit(0)
|
575
|
-
|
641
|
+
|
576
642
|
# Start shutdown task
|
577
643
|
asyncio.create_task(delayed_shutdown())
|
578
|
-
|
644
|
+
|
579
645
|
return {
|
580
646
|
"status": "shutting_down",
|
581
|
-
"message": "Server shutdown initiated. New requests will be rejected."
|
647
|
+
"message": "Server shutdown initiated. New requests will be rejected.",
|
582
648
|
}
|
583
649
|
|
584
650
|
# List of available commands
|
@@ -598,10 +664,10 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
598
664
|
"""
|
599
665
|
# Get request_id from middleware state
|
600
666
|
request_id = getattr(request.state, "request_id", None)
|
601
|
-
|
667
|
+
|
602
668
|
# Create request logger for this endpoint
|
603
669
|
req_logger = RequestLogger(__name__, request_id) if request_id else logger
|
604
|
-
|
670
|
+
|
605
671
|
try:
|
606
672
|
command_info = registry.get_command_info(command_name)
|
607
673
|
return command_info
|
@@ -612,9 +678,9 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
612
678
|
content={
|
613
679
|
"error": {
|
614
680
|
"code": 404,
|
615
|
-
"message": f"Command '{command_name}' not found"
|
681
|
+
"message": f"Command '{command_name}' not found",
|
616
682
|
}
|
617
|
-
}
|
683
|
+
},
|
618
684
|
)
|
619
685
|
|
620
686
|
# Get API tool description
|
@@ -622,39 +688,33 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
622
688
|
async def tool_description_endpoint(tool_name: str, format: Optional[str] = "json"):
|
623
689
|
"""
|
624
690
|
Получить подробное описание инструмента API.
|
625
|
-
|
691
|
+
|
626
692
|
Возвращает полное описание инструмента API с доступными командами,
|
627
693
|
их параметрами и примерами использования. Формат возвращаемых данных
|
628
694
|
может быть JSON или Markdown (text).
|
629
|
-
|
695
|
+
|
630
696
|
Args:
|
631
697
|
tool_name: Имя инструмента API
|
632
698
|
format: Формат вывода (json, text, markdown, html)
|
633
699
|
"""
|
634
700
|
try:
|
635
701
|
description = get_tool_description(tool_name, format)
|
636
|
-
|
702
|
+
|
637
703
|
if format.lower() in ["text", "markdown", "html"]:
|
638
704
|
if format.lower() == "html":
|
639
705
|
return Response(content=description, media_type="text/html")
|
640
706
|
else:
|
641
707
|
return JSONResponse(
|
642
708
|
content={"description": description},
|
643
|
-
media_type="application/json"
|
709
|
+
media_type="application/json",
|
644
710
|
)
|
645
711
|
else:
|
646
712
|
return description
|
647
|
-
|
713
|
+
|
648
714
|
except NotFoundError as e:
|
649
715
|
logger.warning(f"Tool not found: {tool_name}")
|
650
716
|
return JSONResponse(
|
651
|
-
status_code=404,
|
652
|
-
content={
|
653
|
-
"error": {
|
654
|
-
"code": 404,
|
655
|
-
"message": str(e)
|
656
|
-
}
|
657
|
-
}
|
717
|
+
status_code=404, content={"error": {"code": 404, "message": str(e)}}
|
658
718
|
)
|
659
719
|
except Exception as e:
|
660
720
|
logger.exception(f"Error generating tool description: {e}")
|
@@ -663,9 +723,9 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
663
723
|
content={
|
664
724
|
"error": {
|
665
725
|
"code": 500,
|
666
|
-
"message": f"Error generating tool description: {str(e)}"
|
726
|
+
"message": f"Error generating tool description: {str(e)}",
|
667
727
|
}
|
668
|
-
}
|
728
|
+
},
|
669
729
|
)
|
670
730
|
|
671
731
|
# Execute API tool
|
@@ -673,7 +733,7 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
673
733
|
async def execute_tool_endpoint(tool_name: str, params: Dict[str, Any] = Body(...)):
|
674
734
|
"""
|
675
735
|
Выполнить инструмент API с указанными параметрами.
|
676
|
-
|
736
|
+
|
677
737
|
Args:
|
678
738
|
tool_name: Имя инструмента API
|
679
739
|
params: Параметры инструмента
|
@@ -684,24 +744,15 @@ def create_app(title: Optional[str] = None, description: Optional[str] = None, v
|
|
684
744
|
except NotFoundError as e:
|
685
745
|
logger.warning(f"Tool not found: {tool_name}")
|
686
746
|
return JSONResponse(
|
687
|
-
status_code=404,
|
688
|
-
content={
|
689
|
-
"error": {
|
690
|
-
"code": 404,
|
691
|
-
"message": str(e)
|
692
|
-
}
|
693
|
-
}
|
747
|
+
status_code=404, content={"error": {"code": 404, "message": str(e)}}
|
694
748
|
)
|
695
749
|
except Exception as e:
|
696
750
|
logger.exception(f"Error executing tool {tool_name}: {e}")
|
697
751
|
return JSONResponse(
|
698
752
|
status_code=500,
|
699
753
|
content={
|
700
|
-
"error": {
|
701
|
-
|
702
|
-
"message": f"Error executing tool: {str(e)}"
|
703
|
-
}
|
704
|
-
}
|
754
|
+
"error": {"code": 500, "message": f"Error executing tool: {str(e)}"}
|
755
|
+
},
|
705
756
|
)
|
706
|
-
|
757
|
+
|
707
758
|
return app
|