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.

Files changed (212) hide show
  1. mcp_proxy_adapter/__init__.py +10 -0
  2. mcp_proxy_adapter/__main__.py +8 -21
  3. mcp_proxy_adapter/api/app.py +10 -913
  4. mcp_proxy_adapter/api/core/__init__.py +18 -0
  5. mcp_proxy_adapter/api/core/app_factory.py +243 -0
  6. mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
  7. mcp_proxy_adapter/api/core/registration_manager.py +166 -0
  8. mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
  9. mcp_proxy_adapter/api/handlers.py +78 -199
  10. mcp_proxy_adapter/api/middleware/__init__.py +1 -44
  11. mcp_proxy_adapter/api/middleware/base.py +0 -42
  12. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +0 -85
  13. mcp_proxy_adapter/api/middleware/error_handling.py +1 -127
  14. mcp_proxy_adapter/api/middleware/factory.py +0 -94
  15. mcp_proxy_adapter/api/middleware/logging.py +0 -112
  16. mcp_proxy_adapter/api/middleware/performance.py +0 -35
  17. mcp_proxy_adapter/api/middleware/protocol_middleware.py +2 -98
  18. mcp_proxy_adapter/api/middleware/transport_middleware.py +0 -37
  19. mcp_proxy_adapter/api/middleware/unified_security.py +10 -10
  20. mcp_proxy_adapter/api/middleware/user_info_middleware.py +0 -118
  21. mcp_proxy_adapter/api/openapi/__init__.py +21 -0
  22. mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
  23. mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
  24. mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
  25. mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
  26. mcp_proxy_adapter/api/schemas.py +0 -61
  27. mcp_proxy_adapter/api/tool_integration.py +0 -117
  28. mcp_proxy_adapter/api/tools.py +0 -46
  29. mcp_proxy_adapter/cli/__init__.py +12 -0
  30. mcp_proxy_adapter/cli/commands/__init__.py +15 -0
  31. mcp_proxy_adapter/cli/commands/client.py +100 -0
  32. mcp_proxy_adapter/cli/commands/config_generate.py +21 -0
  33. mcp_proxy_adapter/cli/commands/config_validate.py +36 -0
  34. mcp_proxy_adapter/cli/commands/generate.py +259 -0
  35. mcp_proxy_adapter/cli/commands/server.py +174 -0
  36. mcp_proxy_adapter/cli/commands/sets.py +128 -0
  37. mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
  38. mcp_proxy_adapter/cli/examples/__init__.py +8 -0
  39. mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
  40. mcp_proxy_adapter/cli/examples/https_token.py +96 -0
  41. mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
  42. mcp_proxy_adapter/cli/main.py +63 -0
  43. mcp_proxy_adapter/cli/parser.py +324 -0
  44. mcp_proxy_adapter/cli/validators.py +231 -0
  45. mcp_proxy_adapter/client/jsonrpc_client.py +406 -0
  46. mcp_proxy_adapter/client/proxy.py +45 -0
  47. mcp_proxy_adapter/commands/__init__.py +44 -28
  48. mcp_proxy_adapter/commands/auth_validation_command.py +7 -344
  49. mcp_proxy_adapter/commands/base.py +19 -43
  50. mcp_proxy_adapter/commands/builtin_commands.py +0 -75
  51. mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
  52. mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
  53. mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
  54. mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
  55. mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
  56. mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
  57. mcp_proxy_adapter/commands/catalog_manager.py +58 -928
  58. mcp_proxy_adapter/commands/cert_monitor_command.py +0 -88
  59. mcp_proxy_adapter/commands/certificate_management_command.py +0 -45
  60. mcp_proxy_adapter/commands/command_registry.py +172 -904
  61. mcp_proxy_adapter/commands/config_command.py +0 -28
  62. mcp_proxy_adapter/commands/dependency_container.py +1 -70
  63. mcp_proxy_adapter/commands/dependency_manager.py +0 -128
  64. mcp_proxy_adapter/commands/echo_command.py +0 -34
  65. mcp_proxy_adapter/commands/health_command.py +0 -3
  66. mcp_proxy_adapter/commands/help_command.py +0 -159
  67. mcp_proxy_adapter/commands/hooks.py +0 -137
  68. mcp_proxy_adapter/commands/key_management_command.py +0 -25
  69. mcp_proxy_adapter/commands/load_command.py +7 -78
  70. mcp_proxy_adapter/commands/plugins_command.py +0 -16
  71. mcp_proxy_adapter/commands/protocol_management_command.py +0 -28
  72. mcp_proxy_adapter/commands/proxy_registration_command.py +0 -88
  73. mcp_proxy_adapter/commands/queue_commands.py +750 -0
  74. mcp_proxy_adapter/commands/registration_status_command.py +0 -43
  75. mcp_proxy_adapter/commands/registry/__init__.py +18 -0
  76. mcp_proxy_adapter/commands/registry/command_info.py +103 -0
  77. mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
  78. mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
  79. mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
  80. mcp_proxy_adapter/commands/reload_command.py +0 -80
  81. mcp_proxy_adapter/commands/result.py +25 -77
  82. mcp_proxy_adapter/commands/role_test_command.py +0 -44
  83. mcp_proxy_adapter/commands/roles_management_command.py +0 -199
  84. mcp_proxy_adapter/commands/security_command.py +0 -30
  85. mcp_proxy_adapter/commands/settings_command.py +0 -68
  86. mcp_proxy_adapter/commands/ssl_setup_command.py +0 -42
  87. mcp_proxy_adapter/commands/token_management_command.py +0 -1
  88. mcp_proxy_adapter/commands/transport_management_command.py +0 -20
  89. mcp_proxy_adapter/commands/unload_command.py +0 -71
  90. mcp_proxy_adapter/config.py +15 -626
  91. mcp_proxy_adapter/core/__init__.py +5 -39
  92. mcp_proxy_adapter/core/app_factory.py +14 -36
  93. mcp_proxy_adapter/core/app_runner.py +0 -27
  94. mcp_proxy_adapter/core/auth_validator.py +1 -93
  95. mcp_proxy_adapter/core/certificate/__init__.py +20 -0
  96. mcp_proxy_adapter/core/certificate/certificate_creator.py +371 -0
  97. mcp_proxy_adapter/core/certificate/certificate_extractor.py +183 -0
  98. mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
  99. mcp_proxy_adapter/core/certificate/certificate_validator.py +110 -0
  100. mcp_proxy_adapter/core/certificate/ssl_context_manager.py +70 -0
  101. mcp_proxy_adapter/core/certificate_utils.py +64 -903
  102. mcp_proxy_adapter/core/client.py +0 -6
  103. mcp_proxy_adapter/core/client_manager.py +0 -19
  104. mcp_proxy_adapter/core/client_security.py +0 -2
  105. mcp_proxy_adapter/core/config/__init__.py +18 -0
  106. mcp_proxy_adapter/core/config/config.py +195 -0
  107. mcp_proxy_adapter/core/config/config_factory.py +22 -0
  108. mcp_proxy_adapter/core/config/config_loader.py +66 -0
  109. mcp_proxy_adapter/core/config/feature_manager.py +31 -0
  110. mcp_proxy_adapter/core/config/simple_config.py +112 -0
  111. mcp_proxy_adapter/core/config/simple_config_generator.py +50 -0
  112. mcp_proxy_adapter/core/config/simple_config_validator.py +96 -0
  113. mcp_proxy_adapter/core/config_converter.py +0 -186
  114. mcp_proxy_adapter/core/config_validator.py +96 -1238
  115. mcp_proxy_adapter/core/errors.py +7 -42
  116. mcp_proxy_adapter/core/job_manager.py +54 -0
  117. mcp_proxy_adapter/core/logging.py +2 -22
  118. mcp_proxy_adapter/core/mtls_asgi.py +0 -20
  119. mcp_proxy_adapter/core/mtls_asgi_app.py +0 -12
  120. mcp_proxy_adapter/core/mtls_proxy.py +0 -80
  121. mcp_proxy_adapter/core/mtls_server.py +3 -173
  122. mcp_proxy_adapter/core/protocol_manager.py +1 -191
  123. mcp_proxy_adapter/core/proxy/__init__.py +22 -0
  124. mcp_proxy_adapter/core/proxy/auth_manager.py +27 -0
  125. mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +137 -0
  126. mcp_proxy_adapter/core/proxy/registration_client.py +60 -0
  127. mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
  128. mcp_proxy_adapter/core/proxy_client.py +0 -1
  129. mcp_proxy_adapter/core/proxy_registration.py +36 -912
  130. mcp_proxy_adapter/core/role_utils.py +0 -308
  131. mcp_proxy_adapter/core/security_adapter.py +1 -36
  132. mcp_proxy_adapter/core/security_factory.py +1 -150
  133. mcp_proxy_adapter/core/security_integration.py +0 -33
  134. mcp_proxy_adapter/core/server_adapter.py +1 -40
  135. mcp_proxy_adapter/core/server_engine.py +2 -173
  136. mcp_proxy_adapter/core/settings.py +0 -127
  137. mcp_proxy_adapter/core/signal_handler.py +0 -65
  138. mcp_proxy_adapter/core/ssl_utils.py +19 -137
  139. mcp_proxy_adapter/core/transport_manager.py +0 -151
  140. mcp_proxy_adapter/core/unified_config_adapter.py +1 -193
  141. mcp_proxy_adapter/core/utils.py +1 -182
  142. mcp_proxy_adapter/core/validation/__init__.py +21 -0
  143. mcp_proxy_adapter/core/validation/config_validator.py +211 -0
  144. mcp_proxy_adapter/core/validation/file_validator.py +73 -0
  145. mcp_proxy_adapter/core/validation/protocol_validator.py +191 -0
  146. mcp_proxy_adapter/core/validation/security_validator.py +58 -0
  147. mcp_proxy_adapter/core/validation/validation_result.py +27 -0
  148. mcp_proxy_adapter/custom_openapi.py +33 -652
  149. mcp_proxy_adapter/examples/bugfix_certificate_config.py +0 -23
  150. mcp_proxy_adapter/examples/check_config.py +0 -2
  151. mcp_proxy_adapter/examples/client_usage_example.py +164 -0
  152. mcp_proxy_adapter/examples/config_builder.py +13 -2
  153. mcp_proxy_adapter/examples/config_cli.py +0 -1
  154. mcp_proxy_adapter/examples/create_test_configs.py +0 -46
  155. mcp_proxy_adapter/examples/debug_request_state.py +0 -1
  156. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +0 -47
  157. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +0 -45
  158. mcp_proxy_adapter/examples/full_application/commands/echo_command.py +0 -12
  159. mcp_proxy_adapter/examples/full_application/commands/help_command.py +0 -12
  160. mcp_proxy_adapter/examples/full_application/commands/list_command.py +0 -7
  161. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +0 -2
  162. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +0 -59
  163. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +0 -54
  164. mcp_proxy_adapter/examples/full_application/main.py +186 -150
  165. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +0 -107
  166. mcp_proxy_adapter/examples/full_application/test_minimal_server.py +0 -24
  167. mcp_proxy_adapter/examples/full_application/test_server.py +0 -58
  168. mcp_proxy_adapter/examples/generate_config.py +65 -11
  169. mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
  170. mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
  171. mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
  172. mcp_proxy_adapter/examples/queue_server_example.py +85 -0
  173. mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
  174. mcp_proxy_adapter/examples/required_certificates.py +0 -2
  175. mcp_proxy_adapter/examples/run_full_test_suite.py +0 -29
  176. mcp_proxy_adapter/examples/run_proxy_server.py +31 -71
  177. mcp_proxy_adapter/examples/run_security_tests_fixed.py +0 -27
  178. mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
  179. mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
  180. mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
  181. mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
  182. mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
  183. mcp_proxy_adapter/examples/security_test_client.py +24 -1075
  184. mcp_proxy_adapter/examples/setup/__init__.py +24 -0
  185. mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
  186. mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
  187. mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
  188. mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
  189. mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
  190. mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
  191. mcp_proxy_adapter/examples/setup_test_environment.py +133 -1425
  192. mcp_proxy_adapter/examples/test_config.py +0 -3
  193. mcp_proxy_adapter/examples/test_config_builder.py +25 -405
  194. mcp_proxy_adapter/examples/test_examples.py +0 -1
  195. mcp_proxy_adapter/examples/test_framework_complete.py +0 -2
  196. mcp_proxy_adapter/examples/test_mcp_server.py +0 -1
  197. mcp_proxy_adapter/examples/test_protocol_examples.py +0 -1
  198. mcp_proxy_adapter/examples/universal_client.py +0 -6
  199. mcp_proxy_adapter/examples/update_config_certificates.py +0 -1
  200. mcp_proxy_adapter/examples/validate_generator_compatibility.py +0 -1
  201. mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +0 -187
  202. mcp_proxy_adapter/integrations/__init__.py +25 -0
  203. mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
  204. mcp_proxy_adapter/main.py +70 -62
  205. mcp_proxy_adapter/openapi.py +0 -22
  206. mcp_proxy_adapter/version.py +1 -1
  207. {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/METADATA +2 -1
  208. mcp_proxy_adapter-6.9.29.dist-info/RECORD +235 -0
  209. {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/entry_points.txt +1 -1
  210. mcp_proxy_adapter-6.9.27.dist-info/RECORD +0 -149
  211. {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/WHEEL +0 -0
  212. {mcp_proxy_adapter-6.9.27.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/top_level.txt +0 -0
@@ -2,120 +2,14 @@
2
2
  Module for FastAPI application setup.
3
3
  """
4
4
 
5
- import json
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
- logger = logging.getLogger(__name__)
7
+ from fastapi import FastAPI
14
8
 
15
- from fastapi import FastAPI, Body, Depends, HTTPException, Request
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
- @asynccontextmanager
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[ssl.SSLContext]:
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
- current_config = app_config if app_config is not None else config.get_all()
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
- # Use provided configuration or fallback to global config
379
- if app_config is not None:
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)