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.
Files changed (129) hide show
  1. mcp_proxy_adapter/__init__.py +9 -5
  2. mcp_proxy_adapter/__main__.py +1 -1
  3. mcp_proxy_adapter/api/app.py +227 -176
  4. mcp_proxy_adapter/api/handlers.py +68 -60
  5. mcp_proxy_adapter/api/middleware/__init__.py +7 -5
  6. mcp_proxy_adapter/api/middleware/base.py +19 -16
  7. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +44 -34
  8. mcp_proxy_adapter/api/middleware/error_handling.py +57 -67
  9. mcp_proxy_adapter/api/middleware/factory.py +50 -52
  10. mcp_proxy_adapter/api/middleware/logging.py +46 -30
  11. mcp_proxy_adapter/api/middleware/performance.py +19 -16
  12. mcp_proxy_adapter/api/middleware/protocol_middleware.py +80 -50
  13. mcp_proxy_adapter/api/middleware/transport_middleware.py +26 -24
  14. mcp_proxy_adapter/api/middleware/unified_security.py +70 -51
  15. mcp_proxy_adapter/api/middleware/user_info_middleware.py +43 -34
  16. mcp_proxy_adapter/api/schemas.py +69 -43
  17. mcp_proxy_adapter/api/tool_integration.py +83 -63
  18. mcp_proxy_adapter/api/tools.py +60 -50
  19. mcp_proxy_adapter/commands/__init__.py +15 -6
  20. mcp_proxy_adapter/commands/auth_validation_command.py +107 -110
  21. mcp_proxy_adapter/commands/base.py +108 -112
  22. mcp_proxy_adapter/commands/builtin_commands.py +28 -18
  23. mcp_proxy_adapter/commands/catalog_manager.py +394 -265
  24. mcp_proxy_adapter/commands/cert_monitor_command.py +222 -204
  25. mcp_proxy_adapter/commands/certificate_management_command.py +210 -213
  26. mcp_proxy_adapter/commands/command_registry.py +275 -226
  27. mcp_proxy_adapter/commands/config_command.py +48 -33
  28. mcp_proxy_adapter/commands/dependency_container.py +22 -23
  29. mcp_proxy_adapter/commands/dependency_manager.py +65 -56
  30. mcp_proxy_adapter/commands/echo_command.py +15 -15
  31. mcp_proxy_adapter/commands/health_command.py +31 -29
  32. mcp_proxy_adapter/commands/help_command.py +97 -61
  33. mcp_proxy_adapter/commands/hooks.py +65 -49
  34. mcp_proxy_adapter/commands/key_management_command.py +148 -147
  35. mcp_proxy_adapter/commands/load_command.py +58 -40
  36. mcp_proxy_adapter/commands/plugins_command.py +80 -54
  37. mcp_proxy_adapter/commands/protocol_management_command.py +60 -48
  38. mcp_proxy_adapter/commands/proxy_registration_command.py +107 -115
  39. mcp_proxy_adapter/commands/reload_command.py +43 -37
  40. mcp_proxy_adapter/commands/result.py +26 -33
  41. mcp_proxy_adapter/commands/role_test_command.py +26 -26
  42. mcp_proxy_adapter/commands/roles_management_command.py +176 -173
  43. mcp_proxy_adapter/commands/security_command.py +134 -122
  44. mcp_proxy_adapter/commands/settings_command.py +47 -56
  45. mcp_proxy_adapter/commands/ssl_setup_command.py +109 -129
  46. mcp_proxy_adapter/commands/token_management_command.py +129 -158
  47. mcp_proxy_adapter/commands/transport_management_command.py +41 -36
  48. mcp_proxy_adapter/commands/unload_command.py +42 -37
  49. mcp_proxy_adapter/config.py +36 -35
  50. mcp_proxy_adapter/core/__init__.py +19 -21
  51. mcp_proxy_adapter/core/app_factory.py +30 -9
  52. mcp_proxy_adapter/core/app_runner.py +81 -64
  53. mcp_proxy_adapter/core/auth_validator.py +176 -182
  54. mcp_proxy_adapter/core/certificate_utils.py +469 -426
  55. mcp_proxy_adapter/core/client.py +155 -126
  56. mcp_proxy_adapter/core/client_manager.py +60 -54
  57. mcp_proxy_adapter/core/client_security.py +108 -88
  58. mcp_proxy_adapter/core/config_converter.py +176 -143
  59. mcp_proxy_adapter/core/config_validator.py +12 -4
  60. mcp_proxy_adapter/core/crl_utils.py +21 -7
  61. mcp_proxy_adapter/core/errors.py +64 -20
  62. mcp_proxy_adapter/core/logging.py +34 -29
  63. mcp_proxy_adapter/core/mtls_asgi.py +29 -25
  64. mcp_proxy_adapter/core/mtls_asgi_app.py +66 -54
  65. mcp_proxy_adapter/core/protocol_manager.py +154 -104
  66. mcp_proxy_adapter/core/proxy_client.py +202 -144
  67. mcp_proxy_adapter/core/proxy_registration.py +12 -2
  68. mcp_proxy_adapter/core/role_utils.py +139 -125
  69. mcp_proxy_adapter/core/security_adapter.py +88 -77
  70. mcp_proxy_adapter/core/security_factory.py +50 -44
  71. mcp_proxy_adapter/core/security_integration.py +72 -24
  72. mcp_proxy_adapter/core/server_adapter.py +68 -64
  73. mcp_proxy_adapter/core/server_engine.py +71 -53
  74. mcp_proxy_adapter/core/settings.py +68 -58
  75. mcp_proxy_adapter/core/ssl_utils.py +69 -56
  76. mcp_proxy_adapter/core/transport_manager.py +72 -60
  77. mcp_proxy_adapter/core/unified_config_adapter.py +201 -150
  78. mcp_proxy_adapter/core/utils.py +4 -2
  79. mcp_proxy_adapter/custom_openapi.py +107 -99
  80. mcp_proxy_adapter/examples/basic_framework/main.py +9 -2
  81. mcp_proxy_adapter/examples/commands/__init__.py +1 -1
  82. mcp_proxy_adapter/examples/create_certificates_simple.py +182 -71
  83. mcp_proxy_adapter/examples/debug_request_state.py +38 -19
  84. mcp_proxy_adapter/examples/debug_role_chain.py +53 -20
  85. mcp_proxy_adapter/examples/demo_client.py +48 -36
  86. mcp_proxy_adapter/examples/examples/basic_framework/main.py +9 -2
  87. mcp_proxy_adapter/examples/examples/full_application/__init__.py +1 -0
  88. mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +22 -10
  89. mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +24 -17
  90. mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +16 -3
  91. mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +13 -3
  92. mcp_proxy_adapter/examples/examples/full_application/main.py +27 -2
  93. mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +48 -14
  94. mcp_proxy_adapter/examples/full_application/__init__.py +1 -0
  95. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +22 -10
  96. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +24 -17
  97. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +16 -3
  98. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +13 -3
  99. mcp_proxy_adapter/examples/full_application/main.py +27 -2
  100. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +48 -14
  101. mcp_proxy_adapter/examples/generate_all_certificates.py +198 -73
  102. mcp_proxy_adapter/examples/generate_certificates.py +31 -16
  103. mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +220 -74
  104. mcp_proxy_adapter/examples/generate_test_configs.py +68 -91
  105. mcp_proxy_adapter/examples/proxy_registration_example.py +76 -75
  106. mcp_proxy_adapter/examples/run_example.py +23 -5
  107. mcp_proxy_adapter/examples/run_full_test_suite.py +109 -71
  108. mcp_proxy_adapter/examples/run_proxy_server.py +22 -9
  109. mcp_proxy_adapter/examples/run_security_tests.py +103 -41
  110. mcp_proxy_adapter/examples/run_security_tests_fixed.py +72 -36
  111. mcp_proxy_adapter/examples/scripts/config_generator.py +288 -187
  112. mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +185 -72
  113. mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +220 -74
  114. mcp_proxy_adapter/examples/security_test_client.py +196 -127
  115. mcp_proxy_adapter/examples/setup_test_environment.py +17 -29
  116. mcp_proxy_adapter/examples/test_config.py +19 -4
  117. mcp_proxy_adapter/examples/test_config_generator.py +23 -7
  118. mcp_proxy_adapter/examples/test_examples.py +84 -56
  119. mcp_proxy_adapter/examples/universal_client.py +119 -62
  120. mcp_proxy_adapter/openapi.py +108 -115
  121. mcp_proxy_adapter/utils/config_generator.py +429 -274
  122. mcp_proxy_adapter/version.py +1 -2
  123. {mcp_proxy_adapter-6.3.3.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/METADATA +1 -1
  124. mcp_proxy_adapter-6.3.5.dist-info/RECORD +143 -0
  125. mcp_proxy_adapter-6.3.3.dist-info/RECORD +0 -143
  126. {mcp_proxy_adapter-6.3.3.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/WHEEL +0 -0
  127. {mcp_proxy_adapter-6.3.3.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/entry_points.txt +0 -0
  128. {mcp_proxy_adapter-6.3.3.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/licenses/LICENSE +0 -0
  129. {mcp_proxy_adapter-6.3.3.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/top_level.txt +0 -0
@@ -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 execute_command, handle_json_rpc, handle_batch_json_rpc, get_server_health, get_commands_list
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 JsonRpcRequest, JsonRpcSuccessResponse, JsonRpcErrorResponse, HealthResponse, CommandListResponse, APIToolDescription
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(f"Application started with {init_result['total_commands']} commands registered")
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(app_config: Optional[Dict[str, Any]] = None) -> Optional[ssl.SSLContext]:
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(title: Optional[str] = None, description: Optional[str] = None, version: Optional[str] = None, app_config: Optional[Dict[str, Any]] = None, config_path: Optional[str] = None) -> FastAPI:
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, 'get_all'):
225
+ if hasattr(app_config, "get_all"):
200
226
  current_config = app_config.get_all()
201
- elif hasattr(app_config, 'keys'):
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, 'keys'):
211
- print(f"🔍 Debug: create_app received app_config keys: {list(app_config.keys())}")
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(f"🔍 Debug: create_app SSL config: enabled={ssl_config.get('enabled', False)}")
215
- print(f"🔍 Debug: create_app SSL config: cert_file={ssl_config.get('cert_file')}")
216
- print(f"🔍 Debug: create_app SSL config: key_file={ssl_config.get('key_file')}")
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(f"🔍 Debug: api/app.py security.ssl: cert_file={cert_file}, key_file={key_file}")
249
- print(f"🔍 Debug: api/app.py security.ssl: cert_file exists={Path(cert_file).exists() if cert_file else 'None'}")
250
- print(f"🔍 Debug: api/app.py security.ssl: key_file exists={Path(key_file).exists() if key_file else 'None'}")
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(f"SSL is enabled but certificate file not found: {cert_file}")
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(f"SSL is enabled but private key file not found: {key_file}")
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(f"mTLS is enabled but CA certificate file not found: {ca_cert_file}")
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(f"Roles are enabled but schema file not found: {roles_config_path}")
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(f"Permissions are enabled but roles file not found: {roles_file}")
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(f"🔍 Debug: api/app.py legacy.ssl: cert_file={cert_file}, key_file={key_file}")
287
- print(f"🔍 Debug: api/app.py legacy.ssl: cert_file exists={Path(cert_file).exists() if cert_file else 'None'}")
288
- print(f"🔍 Debug: api/app.py legacy.ssl: key_file exists={Path(key_file).exists() if key_file else 'None'}")
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(f"Legacy SSL is enabled but certificate file not found: {cert_file}")
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(f"Legacy SSL is enabled but private key file not found: {key_file}")
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(f"Legacy mTLS is enabled but CA certificate file not found: {ca_cert}")
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(f"Token authentication is enabled but tokens file not found: {tokens_file}")
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("Authentication is enabled but no authentication method is properly configured")
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("CRITICAL SECURITY ERROR: Authentication configuration issues detected:")
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("Please fix authentication configuration or disable authentication features.")
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("/api/jsonrpc", response_model=Union[JsonRpcSuccessResponse, JsonRpcErrorResponse, List[Union[JsonRpcSuccessResponse, JsonRpcErrorResponse]]])
368
- async def jsonrpc_endpoint(request: Request, request_data: Union[Dict[str, Any], List[Dict[str, Any]]] = Body(...)):
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(f"Executing command via /cmd: {command_name}, params: {params}")
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(command_name, params, request_id, request)
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": "To get detailed information about a specific command, call help with parameter: POST /cmd {\"command\": \"help\", \"params\": {\"cmdname\": \"<command_name>\"}}"
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(request: Request, command_name: str, params: Dict[str, Any] = Body(default={})):
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
- "status": "ok",
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
- "code": 500,
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