mcp-proxy-adapter 4.1.1__py3-none-any.whl → 6.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. mcp_proxy_adapter/__main__.py +12 -0
  2. mcp_proxy_adapter/api/app.py +254 -33
  3. mcp_proxy_adapter/api/handlers.py +32 -6
  4. mcp_proxy_adapter/api/middleware/__init__.py +36 -30
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +243 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
  10. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  11. mcp_proxy_adapter/api/middleware/unified_security.py +152 -0
  12. mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -0
  13. mcp_proxy_adapter/commands/__init__.py +19 -4
  14. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  15. mcp_proxy_adapter/commands/base.py +66 -32
  16. mcp_proxy_adapter/commands/builtin_commands.py +95 -0
  17. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  18. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  19. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  20. mcp_proxy_adapter/commands/command_registry.py +711 -354
  21. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  22. mcp_proxy_adapter/commands/echo_command.py +81 -0
  23. mcp_proxy_adapter/commands/health_command.py +7 -0
  24. mcp_proxy_adapter/commands/help_command.py +21 -14
  25. mcp_proxy_adapter/commands/hooks.py +200 -167
  26. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  27. mcp_proxy_adapter/commands/load_command.py +176 -0
  28. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  29. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  30. mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
  31. mcp_proxy_adapter/commands/reload_command.py +48 -50
  32. mcp_proxy_adapter/commands/result.py +1 -0
  33. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  34. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  35. mcp_proxy_adapter/commands/security_command.py +488 -0
  36. mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
  37. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  38. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  39. mcp_proxy_adapter/commands/unload_command.py +158 -0
  40. mcp_proxy_adapter/config.py +159 -2
  41. mcp_proxy_adapter/core/app_factory.py +326 -0
  42. mcp_proxy_adapter/core/auth_validator.py +606 -0
  43. mcp_proxy_adapter/core/certificate_utils.py +827 -0
  44. mcp_proxy_adapter/core/client_security.py +384 -0
  45. mcp_proxy_adapter/core/config_converter.py +405 -0
  46. mcp_proxy_adapter/core/config_validator.py +218 -0
  47. mcp_proxy_adapter/core/logging.py +19 -3
  48. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  49. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  50. mcp_proxy_adapter/core/protocol_manager.py +235 -0
  51. mcp_proxy_adapter/core/proxy_client.py +602 -0
  52. mcp_proxy_adapter/core/proxy_registration.py +522 -0
  53. mcp_proxy_adapter/core/role_utils.py +426 -0
  54. mcp_proxy_adapter/core/security_adapter.py +370 -0
  55. mcp_proxy_adapter/core/security_factory.py +239 -0
  56. mcp_proxy_adapter/core/security_integration.py +277 -0
  57. mcp_proxy_adapter/core/server_adapter.py +345 -0
  58. mcp_proxy_adapter/core/server_engine.py +364 -0
  59. mcp_proxy_adapter/core/settings.py +1 -0
  60. mcp_proxy_adapter/core/ssl_utils.py +233 -0
  61. mcp_proxy_adapter/core/transport_manager.py +292 -0
  62. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  63. mcp_proxy_adapter/custom_openapi.py +22 -11
  64. mcp_proxy_adapter/examples/README.md +230 -97
  65. mcp_proxy_adapter/examples/README_EN.md +258 -0
  66. mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
  67. mcp_proxy_adapter/examples/__pycache__/security_configurations.cpython-312.pyc +0 -0
  68. mcp_proxy_adapter/examples/__pycache__/security_test_client.cpython-312.pyc +0 -0
  69. mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
  70. mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
  71. mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +39 -0
  72. mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +25 -0
  73. mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
  74. mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
  75. mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
  76. mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
  77. mcp_proxy_adapter/examples/cert_config.json +9 -0
  78. mcp_proxy_adapter/examples/certs/admin.crt +32 -0
  79. mcp_proxy_adapter/examples/certs/admin.key +52 -0
  80. mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
  81. mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
  82. mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
  83. mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
  84. mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
  85. mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
  86. mcp_proxy_adapter/examples/certs/client.crt +32 -0
  87. mcp_proxy_adapter/examples/certs/client.key +52 -0
  88. mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
  89. mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
  90. mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
  91. mcp_proxy_adapter/examples/certs/client_user.key +52 -0
  92. mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
  93. mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
  94. mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
  95. mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
  96. mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
  97. mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
  98. mcp_proxy_adapter/examples/certs/readonly.key +52 -0
  99. mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
  100. mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
  101. mcp_proxy_adapter/examples/certs/server.crt +32 -0
  102. mcp_proxy_adapter/examples/certs/server.key +52 -0
  103. mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
  104. mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
  105. mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
  106. mcp_proxy_adapter/examples/certs/user.crt +32 -0
  107. mcp_proxy_adapter/examples/certs/user.key +52 -0
  108. mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
  109. mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
  110. mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
  111. mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
  112. mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
  113. mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
  114. mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
  115. mcp_proxy_adapter/examples/commands/__init__.py +1 -0
  116. mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
  117. mcp_proxy_adapter/examples/debug_request_state.py +144 -0
  118. mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
  119. mcp_proxy_adapter/examples/demo_client.py +341 -0
  120. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
  121. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
  122. mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
  123. mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
  124. mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
  125. mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
  126. mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
  127. mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
  128. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
  129. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
  130. mcp_proxy_adapter/examples/full_application/main.py +138 -0
  131. mcp_proxy_adapter/examples/full_application/roles.json +21 -0
  132. mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
  133. mcp_proxy_adapter/examples/generate_certificates.py +121 -0
  134. mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
  135. mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
  136. mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
  137. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
  138. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
  139. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
  140. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
  141. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
  142. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
  143. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
  144. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
  145. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
  146. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
  147. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
  148. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
  149. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
  150. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
  151. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
  152. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
  153. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
  154. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
  155. mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
  156. mcp_proxy_adapter/examples/roles.json +38 -0
  157. mcp_proxy_adapter/examples/run_example.py +81 -0
  158. mcp_proxy_adapter/examples/run_security_tests.py +326 -0
  159. mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
  160. mcp_proxy_adapter/examples/security_test_client.py +743 -0
  161. mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
  162. mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
  163. mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
  164. mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
  165. mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
  166. mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
  167. mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
  168. mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
  169. mcp_proxy_adapter/examples/test_examples.py +344 -0
  170. mcp_proxy_adapter/examples/universal_client.py +628 -0
  171. mcp_proxy_adapter/main.py +186 -0
  172. mcp_proxy_adapter/utils/config_generator.py +639 -0
  173. mcp_proxy_adapter/version.py +2 -1
  174. mcp_proxy_adapter-6.1.0.dist-info/METADATA +205 -0
  175. mcp_proxy_adapter-6.1.0.dist-info/RECORD +193 -0
  176. mcp_proxy_adapter-6.1.0.dist-info/entry_points.txt +2 -0
  177. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/licenses/LICENSE +2 -2
  178. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  179. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  180. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  181. mcp_proxy_adapter/examples/__init__.py +0 -7
  182. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  183. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  184. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  185. mcp_proxy_adapter/examples/basic_server/config.json +0 -35
  186. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  187. mcp_proxy_adapter/examples/basic_server/server.py +0 -103
  188. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  189. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  190. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
  191. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  192. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  193. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  194. mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
  195. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  196. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  197. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  198. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  199. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  200. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  201. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  202. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  203. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  204. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  205. mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
  206. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  207. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  208. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  209. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  210. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  211. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  212. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  213. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  214. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  215. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  216. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  217. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  218. mcp_proxy_adapter/tests/__init__.py +0 -0
  219. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  220. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  221. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  222. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  223. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  224. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  225. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  226. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  227. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  228. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  229. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  230. mcp_proxy_adapter/tests/conftest.py +0 -131
  231. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  232. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  233. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  234. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  235. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  236. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  237. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  238. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  239. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  240. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  241. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  242. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  243. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  244. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  245. mcp_proxy_adapter/tests/test_config.py +0 -127
  246. mcp_proxy_adapter/tests/test_utils.py +0 -65
  247. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  248. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  249. mcp_proxy_adapter/tests/unit/test_config.py +0 -217
  250. mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
  251. mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
  252. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/WHEEL +0 -0
  253. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/top_level.txt +0 -0
@@ -18,15 +18,29 @@ Example: Registering a command instance (for dependency injection)
18
18
  """
19
19
 
20
20
  import importlib
21
+ import importlib.util
21
22
  import inspect
22
23
  import os
23
24
  import pkgutil
25
+ import tempfile
26
+ import urllib.parse
27
+ from pathlib import Path
24
28
  from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast
25
29
 
26
30
  from mcp_proxy_adapter.commands.base import Command
31
+ from mcp_proxy_adapter.commands.hooks import hooks
32
+ from mcp_proxy_adapter.commands.catalog_manager import CatalogManager
33
+ from mcp_proxy_adapter.config import config
27
34
  from mcp_proxy_adapter.core.errors import NotFoundError
28
35
  from mcp_proxy_adapter.core.logging import logger
29
36
 
37
+ try:
38
+ import requests
39
+ REQUESTS_AVAILABLE = True
40
+ except ImportError:
41
+ REQUESTS_AVAILABLE = False
42
+ logger.warning("requests library not available, HTTP/HTTPS loading will not work")
43
+
30
44
  T = TypeVar("T", bound=Command)
31
45
 
32
46
 
@@ -39,16 +53,107 @@ class CommandRegistry:
39
53
  """
40
54
  Initialize command registry.
41
55
  """
42
- self._commands: Dict[str, Type[Command]] = {}
43
- self._instances: Dict[str, Command] = {}
44
- self._custom_commands: Dict[str, Type[Command]] = {} # Custom commands with priority
56
+ self._builtin_commands: Dict[str, Type[Command]] = {} # Built-in framework commands
57
+ self._custom_commands: Dict[str, Type[Command]] = {} # Custom commands (highest priority)
58
+ self._loaded_commands: Dict[str, Type[Command]] = {} # Commands loaded from directory
59
+ self._instances: Dict[str, Command] = {} # Command instances
60
+
61
+ def register_builtin(self, command: Union[Type[Command], Command]) -> None:
62
+ """
63
+ Register a built-in framework command.
64
+
65
+ Args:
66
+ command: Command class or instance to register.
67
+
68
+ Raises:
69
+ ValueError: If command with the same name is already registered.
70
+ """
71
+ command_name = self._get_command_name(command)
72
+
73
+ # Check for conflicts with other built-in commands
74
+ if command_name in self._builtin_commands:
75
+ logger.error(f"Built-in command '{command_name}' is already registered, skipping")
76
+ raise ValueError(f"Built-in command '{command_name}' is already registered")
77
+
78
+ # Built-in commands can override loaded commands
79
+ # Remove any existing loaded commands with the same name
80
+ if command_name in self._loaded_commands:
81
+ logger.info(f"Built-in command '{command_name}' overrides loaded command")
82
+ del self._loaded_commands[command_name]
83
+
84
+ self._register_command(command, self._builtin_commands, "built-in")
85
+
86
+ def register_custom(self, command: Union[Type[Command], Command]) -> None:
87
+ """
88
+ Register a custom command with highest priority.
89
+
90
+ Args:
91
+ command: Command class or instance to register.
92
+
93
+ Raises:
94
+ ValueError: If command with the same name is already registered.
95
+ """
96
+ command_name = self._get_command_name(command)
97
+
98
+ # Check for conflicts with other custom commands
99
+ if command_name in self._custom_commands:
100
+ logger.error(f"Custom command '{command_name}' is already registered, skipping")
101
+ raise ValueError(f"Custom command '{command_name}' is already registered")
102
+
103
+ # Custom commands can override built-in and loaded commands
104
+ # Remove any existing commands with the same name from other types
105
+ if command_name in self._builtin_commands:
106
+ logger.info(f"Custom command '{command_name}' overrides built-in command")
107
+ del self._builtin_commands[command_name]
108
+
109
+ if command_name in self._loaded_commands:
110
+ logger.info(f"Custom command '{command_name}' overrides loaded command")
111
+ del self._loaded_commands[command_name]
112
+
113
+ self._register_command(command, self._custom_commands, "custom")
114
+
115
+ def register_loaded(self, command: Union[Type[Command], Command]) -> None:
116
+ """
117
+ Register a command loaded from directory.
118
+
119
+ Args:
120
+ command: Command class or instance to register.
121
+
122
+ Returns:
123
+ bool: True if registered, False if skipped due to conflict.
124
+ """
125
+ command_name = self._get_command_name(command)
126
+
127
+ # Check for conflicts with custom and built-in commands
128
+ if command_name in self._custom_commands:
129
+ logger.warning(f"Loaded command '{command_name}' conflicts with custom command, skipping")
130
+ return False
131
+
132
+ if command_name in self._builtin_commands:
133
+ logger.warning(f"Loaded command '{command_name}' conflicts with built-in command, skipping")
134
+ return False
135
+
136
+ # Check for conflicts within loaded commands
137
+ if command_name in self._loaded_commands:
138
+ logger.warning(f"Loaded command '{command_name}' already exists, skipping duplicate")
139
+ return False
140
+
141
+ try:
142
+ self._register_command(command, self._loaded_commands, "loaded")
143
+ return True
144
+ except ValueError:
145
+ return False
45
146
 
46
- def register(self, command: Union[Type[Command], Command]) -> None:
147
+ def _register_command(self, command: Union[Type[Command], Command],
148
+ target_dict: Dict[str, Type[Command]],
149
+ command_type: str) -> None:
47
150
  """
48
- Registers command class or instance in the registry.
151
+ Internal method to register a command in the specified dictionary.
49
152
 
50
153
  Args:
51
154
  command: Command class or instance to register.
155
+ target_dict: Dictionary to register the command in.
156
+ command_type: Type of command for logging.
52
157
 
53
158
  Raises:
54
159
  ValueError: If command with the same name is already registered.
@@ -63,7 +168,29 @@ class CommandRegistry:
63
168
  else:
64
169
  raise ValueError(f"Invalid command type: {type(command)}. Expected Command class or instance.")
65
170
 
66
- # Get command name
171
+ command_name = self._get_command_name(command_class)
172
+
173
+ if command_name in target_dict:
174
+ raise ValueError(f"{command_type.capitalize()} command '{command_name}' is already registered")
175
+
176
+ logger.debug(f"Registering {command_type} command: {command_name}")
177
+ target_dict[command_name] = command_class
178
+
179
+ # Store instance if provided
180
+ if command_instance:
181
+ logger.debug(f"Storing {command_type} instance for command: {command_name}")
182
+ self._instances[command_name] = command_instance
183
+
184
+ def _get_command_name(self, command_class: Type[Command]) -> str:
185
+ """
186
+ Get command name from command class.
187
+
188
+ Args:
189
+ command_class: Command class.
190
+
191
+ Returns:
192
+ Command name.
193
+ """
67
194
  if not hasattr(command_class, "name") or not command_class.name:
68
195
  # Use class name if name attribute is not set
69
196
  command_name = command_class.__name__.lower()
@@ -71,42 +198,276 @@ class CommandRegistry:
71
198
  command_name = command_name[:-7] # Remove "command" suffix
72
199
  else:
73
200
  command_name = command_class.name
201
+
202
+ return command_name
203
+
204
+
205
+
206
+
207
+
208
+ def load_command_from_source(self, source: str) -> Dict[str, Any]:
209
+ """
210
+ Universal command loader - handles local files, URLs, and remote registry.
211
+
212
+ Args:
213
+ source: Source string - local path, URL, or command name from registry
74
214
 
75
- if command_name in self._commands:
76
- logger.debug(f"Command '{command_name}' is already registered, skipping")
77
- raise ValueError(f"Command '{command_name}' is already registered")
215
+ Returns:
216
+ Dictionary with loading result information
217
+ """
218
+ logger.info(f"Loading command from source: {source}")
219
+
220
+ # Parse source to determine type
221
+ parsed_url = urllib.parse.urlparse(source)
222
+ is_url = parsed_url.scheme in ('http', 'https')
223
+
224
+ if is_url:
225
+ # URL - always download and load
226
+ return self._load_command_from_url(source)
227
+ else:
228
+ # Local path or command name - check remote registry first
229
+ return self._load_command_with_registry_check(source)
230
+
231
+ def _load_command_with_registry_check(self, source: str) -> Dict[str, Any]:
232
+ """
233
+ Load command with remote registry check.
234
+
235
+ Args:
236
+ source: Local path or command name
237
+
238
+ Returns:
239
+ Dictionary with loading result information
240
+ """
241
+ try:
242
+ from mcp_proxy_adapter.commands.catalog_manager import CatalogManager
78
243
 
79
- logger.debug(f"Registering command: {command_name}")
80
- self._commands[command_name] = command_class
244
+ # Get remote registry
245
+ plugin_servers = config.get("commands.plugin_servers", [])
246
+ catalog_dir = "./catalog"
247
+
248
+ if plugin_servers:
249
+ # Initialize catalog manager
250
+ catalog_manager = CatalogManager(catalog_dir)
251
+
252
+ # Check if source is a command name in registry
253
+ if not os.path.exists(source) and not source.endswith('_command.py'):
254
+ # Try to find in remote registry
255
+ for server_url in plugin_servers:
256
+ try:
257
+ server_catalog = catalog_manager.get_catalog_from_server(server_url)
258
+ if source in server_catalog:
259
+ server_cmd = server_catalog[source]
260
+ # Download from registry
261
+ if catalog_manager._download_command(source, server_cmd):
262
+ source = str(catalog_manager.commands_dir / f"{source}_command.py")
263
+ break
264
+ except Exception as e:
265
+ logger.warning(f"Failed to check registry {server_url}: {e}")
266
+
267
+ # Load from local file
268
+ return self._load_command_from_file(source)
269
+
270
+ except Exception as e:
271
+ logger.error(f"Failed to load command with registry check: {e}")
272
+ return {
273
+ "success": False,
274
+ "commands_loaded": 0,
275
+ "error": str(e)
276
+ }
277
+
278
+ def _load_command_from_url(self, url: str) -> Dict[str, Any]:
279
+ """
280
+ Load command from HTTP/HTTPS URL.
81
281
 
82
- # Store instance if provided
83
- if command_instance:
84
- logger.debug(f"Storing instance for command: {command_name}")
85
- self._instances[command_name] = command_instance
282
+ Args:
283
+ url: URL to load command from
284
+
285
+ Returns:
286
+ Dictionary with loading result information
287
+ """
288
+ if not REQUESTS_AVAILABLE:
289
+ error_msg = "requests library not available, cannot load from URL"
290
+ logger.error(error_msg)
291
+ return {
292
+ "success": False,
293
+ "error": error_msg,
294
+ "commands_loaded": 0,
295
+ "source": url
296
+ }
297
+
298
+ try:
299
+ logger.debug(f"Downloading command from URL: {url}")
300
+ response = requests.get(url, timeout=30)
301
+ response.raise_for_status()
302
+
303
+ # Get filename from URL or use default
304
+ filename = os.path.basename(urllib.parse.urlparse(url).path)
305
+ if not filename or not filename.endswith('.py'):
306
+ filename = 'remote_command.py'
307
+
308
+ # Create temporary file
309
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
310
+ temp_file.write(response.text)
311
+ temp_file_path = temp_file.name
312
+
313
+ try:
314
+ # Load command from temporary file
315
+ result = self._load_command_from_file(temp_file_path, is_temporary=True)
316
+ result["source"] = url
317
+ return result
318
+ finally:
319
+ # Clean up temporary file
320
+ try:
321
+ os.unlink(temp_file_path)
322
+ except Exception as e:
323
+ logger.warning(f"Failed to clean up temporary file {temp_file_path}: {e}")
324
+
325
+ except Exception as e:
326
+ error_msg = f"Failed to load command from URL {url}: {e}"
327
+ logger.error(error_msg)
328
+ return {
329
+ "success": False,
330
+ "error": error_msg,
331
+ "commands_loaded": 0,
332
+ "source": url
333
+ }
86
334
 
87
- def unregister(self, command_name: str) -> None:
335
+ def _load_command_from_file(self, file_path: str, is_temporary: bool = False) -> Dict[str, Any]:
88
336
  """
89
- Removes command from registry.
90
-
337
+ Load command from local file.
338
+
91
339
  Args:
92
- command_name: Command name to remove.
93
-
94
- Raises:
95
- NotFoundError: If command is not found.
340
+ file_path: Path to command file
341
+ is_temporary: Whether this is a temporary file (for cleanup)
342
+
343
+ Returns:
344
+ Dictionary with loading result information
345
+ """
346
+ if not os.path.exists(file_path):
347
+ error_msg = f"Command file does not exist: {file_path}"
348
+ logger.error(error_msg)
349
+ return {
350
+ "success": False,
351
+ "error": error_msg,
352
+ "commands_loaded": 0,
353
+ "source": file_path
354
+ }
355
+
356
+ # For temporary files (downloaded from URL), we don't enforce the _command.py naming
357
+ # since the original filename is preserved in the URL
358
+ if not is_temporary and not file_path.endswith('_command.py'):
359
+ error_msg = f"Command file must end with '_command.py': {file_path}"
360
+ logger.error(error_msg)
361
+ return {
362
+ "success": False,
363
+ "error": error_msg,
364
+ "commands_loaded": 0,
365
+ "source": file_path
366
+ }
367
+
368
+ try:
369
+ module_name = os.path.basename(file_path)[:-3] # Remove .py extension
370
+ logger.debug(f"Loading command from file: {file_path}")
371
+
372
+ # Load module from file
373
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
374
+ if spec and spec.loader:
375
+ module = importlib.util.module_from_spec(spec)
376
+ spec.loader.exec_module(module)
377
+
378
+ commands_loaded = 0
379
+ loaded_commands = []
380
+
381
+ # Find command classes in the module
382
+ for name, obj in inspect.getmembers(module):
383
+ if (inspect.isclass(obj) and
384
+ issubclass(obj, Command) and
385
+ obj != Command and
386
+ not inspect.isabstract(obj)):
387
+
388
+ command_name = self._get_command_name(obj)
389
+ if self.register_loaded(cast(Type[Command], obj)):
390
+ commands_loaded += 1
391
+ loaded_commands.append(command_name)
392
+ logger.debug(f"Loaded command: {command_name}")
393
+ else:
394
+ logger.debug(f"Skipped command: {command_name}")
395
+
396
+ return {
397
+ "success": True,
398
+ "commands_loaded": commands_loaded,
399
+ "loaded_commands": loaded_commands,
400
+ "source": file_path
401
+ }
402
+ else:
403
+ error_msg = f"Failed to create module spec for: {file_path}"
404
+ logger.error(error_msg)
405
+ return {
406
+ "success": False,
407
+ "error": error_msg,
408
+ "commands_loaded": 0,
409
+ "source": file_path
410
+ }
411
+
412
+ except Exception as e:
413
+ error_msg = f"Error loading command from file {file_path}: {e}"
414
+ logger.error(error_msg)
415
+ return {
416
+ "success": False,
417
+ "error": error_msg,
418
+ "commands_loaded": 0,
419
+ "source": file_path
420
+ }
421
+
422
+ def unload_command(self, command_name: str) -> Dict[str, Any]:
96
423
  """
97
- if command_name not in self._commands:
98
- raise NotFoundError(f"Command '{command_name}' not found")
424
+ Unload a loaded command from registry.
425
+
426
+ Args:
427
+ command_name: Name of the command to unload
99
428
 
100
- logger.debug(f"Unregistering command: {command_name}")
101
- del self._commands[command_name]
429
+ Returns:
430
+ Dictionary with unloading result information
431
+ """
432
+ logger.info(f"Unloading command: {command_name}")
102
433
 
103
- # Remove instance if exists
104
- if command_name in self._instances:
105
- del self._instances[command_name]
434
+ # Check if command exists in loaded commands
435
+ if command_name not in self._loaded_commands:
436
+ error_msg = f"Command '{command_name}' is not a loaded command or does not exist"
437
+ logger.warning(error_msg)
438
+ return {
439
+ "success": False,
440
+ "error": error_msg,
441
+ "command_name": command_name
442
+ }
443
+
444
+ try:
445
+ # Remove from loaded commands
446
+ del self._loaded_commands[command_name]
447
+
448
+ # Remove instance if exists
449
+ if command_name in self._instances:
450
+ del self._instances[command_name]
451
+
452
+ logger.info(f"Successfully unloaded command: {command_name}")
453
+ return {
454
+ "success": True,
455
+ "command_name": command_name,
456
+ "message": f"Command '{command_name}' unloaded successfully"
457
+ }
458
+
459
+ except Exception as e:
460
+ error_msg = f"Failed to unload command '{command_name}': {e}"
461
+ logger.error(error_msg)
462
+ return {
463
+ "success": False,
464
+ "error": error_msg,
465
+ "command_name": command_name
466
+ }
106
467
 
107
468
  def command_exists(self, command_name: str) -> bool:
108
469
  """
109
- Checks if command exists in registry.
470
+ Check if command exists with priority order.
110
471
 
111
472
  Args:
112
473
  command_name: Command name to check.
@@ -114,11 +475,13 @@ class CommandRegistry:
114
475
  Returns:
115
476
  True if command exists, False otherwise.
116
477
  """
117
- return command_name in self._commands
478
+ return (command_name in self._custom_commands or
479
+ command_name in self._builtin_commands or
480
+ command_name in self._loaded_commands)
118
481
 
119
482
  def get_command(self, command_name: str) -> Type[Command]:
120
483
  """
121
- Gets command class by name.
484
+ Get command class with priority order.
122
485
 
123
486
  Args:
124
487
  command_name: Command name.
@@ -129,14 +492,19 @@ class CommandRegistry:
129
492
  Raises:
130
493
  NotFoundError: If command is not found.
131
494
  """
132
- if command_name not in self._commands:
495
+ # Check in priority order: custom -> built-in -> loaded
496
+ if command_name in self._custom_commands:
497
+ return self._custom_commands[command_name]
498
+ elif command_name in self._builtin_commands:
499
+ return self._builtin_commands[command_name]
500
+ elif command_name in self._loaded_commands:
501
+ return self._loaded_commands[command_name]
502
+ else:
133
503
  raise NotFoundError(f"Command '{command_name}' not found")
134
-
135
- return self._commands[command_name]
136
-
504
+
137
505
  def get_command_instance(self, command_name: str) -> Command:
138
506
  """
139
- Gets command instance by name. If instance doesn't exist, creates new one.
507
+ Get command instance by name. If instance doesn't exist, creates new one.
140
508
 
141
509
  Args:
142
510
  command_name: Command name
@@ -147,17 +515,16 @@ class CommandRegistry:
147
515
  Raises:
148
516
  NotFoundError: If command is not found
149
517
  """
150
- if command_name not in self._commands:
518
+ if not self.command_exists(command_name):
151
519
  raise NotFoundError(f"Command '{command_name}' not found")
152
520
 
153
521
  # Return existing instance if available
154
522
  if command_name in self._instances:
155
523
  return self._instances[command_name]
156
524
 
157
- # Otherwise create new instance without dependencies
158
- # (this will raise error if command requires dependencies)
525
+ # Otherwise create new instance
159
526
  try:
160
- command_class = self._commands[command_name]
527
+ command_class = self.get_command(command_name)
161
528
  return command_class()
162
529
  except Exception as e:
163
530
  logger.error(f"Failed to create instance of '{command_name}': {e}")
@@ -165,402 +532,392 @@ class CommandRegistry:
165
532
 
166
533
  def has_instance(self, command_name: str) -> bool:
167
534
  """
168
- Checks if command instance exists in registry.
535
+ Check if command has a registered instance.
169
536
 
170
537
  Args:
171
538
  command_name: Command name
172
539
 
173
540
  Returns:
174
- True if command instance exists, False otherwise
541
+ True if command has instance, False otherwise
175
542
  """
176
543
  return command_name in self._instances
177
544
 
178
545
  def get_all_commands(self) -> Dict[str, Type[Command]]:
179
546
  """
180
- Returns all registered commands.
547
+ Get all registered commands with priority order.
181
548
 
182
549
  Returns:
183
550
  Dictionary with command names and their classes.
184
551
  """
185
- return dict(self._commands)
552
+ all_commands = {}
553
+
554
+ # Add commands in priority order: custom -> built-in -> loaded
555
+ # Custom commands override built-in and loaded
556
+ all_commands.update(self._custom_commands)
557
+
558
+ # Built-in commands (only if not overridden by custom)
559
+ for name, command_class in self._builtin_commands.items():
560
+ if name not in all_commands:
561
+ all_commands[name] = command_class
562
+
563
+ # Loaded commands (only if not overridden by custom or built-in)
564
+ for name, command_class in self._loaded_commands.items():
565
+ if name not in all_commands:
566
+ all_commands[name] = command_class
567
+
568
+ return all_commands
186
569
 
187
- def get_command_info(self, command_name: str) -> Dict[str, Any]:
570
+ def get_commands_by_type(self) -> Dict[str, Dict[str, Type[Command]]]:
188
571
  """
189
- Gets information about a command.
190
-
191
- Args:
192
- command_name: Command name.
572
+ Get commands grouped by type.
193
573
 
194
574
  Returns:
195
- Dictionary with command information.
196
-
197
- Raises:
198
- NotFoundError: If command is not found.
575
+ Dictionary with commands grouped by type.
199
576
  """
200
- command_class = self.get_command_with_priority(command_name)
201
-
202
577
  return {
203
- "name": command_name,
204
- "description": command_class.__doc__ or "",
205
- "params": command_class.get_param_info(),
206
- "schema": command_class.get_schema(),
207
- "result_schema": command_class.get_result_schema()
578
+ "custom": self._custom_commands,
579
+ "builtin": self._builtin_commands,
580
+ "loaded": self._loaded_commands
208
581
  }
209
582
 
210
- def get_command_metadata(self, command_name: str) -> Dict[str, Any]:
211
- """
212
- Get complete metadata for a command.
213
-
214
- Args:
215
- command_name: Command name
216
-
217
- Returns:
218
- Dict with command metadata
219
-
220
- Raises:
221
- NotFoundError: If command is not found
222
- """
223
- command_class = self.get_command_with_priority(command_name)
224
- return command_class.get_metadata()
225
-
226
583
  def get_all_metadata(self) -> Dict[str, Dict[str, Any]]:
227
584
  """
228
585
  Get metadata for all registered commands.
229
586
 
230
587
  Returns:
231
- Dict with command names as keys and metadata as values
588
+ Dictionary with command names as keys and metadata as values.
232
589
  """
233
590
  metadata = {}
234
- # Add custom commands first (they have priority)
235
- for name, command_class in self._custom_commands.items():
236
- metadata[name] = command_class.get_metadata()
237
- # Add built-in commands (custom commands will override if same name)
238
- for name, command_class in self._commands.items():
239
- if name not in self._custom_commands: # Only add if not overridden by custom
240
- metadata[name] = command_class.get_metadata()
591
+
592
+ # Get all commands with priority order
593
+ all_commands = self.get_all_commands()
594
+
595
+ for command_name, command_class in all_commands.items():
596
+ try:
597
+ # Get command metadata
598
+ if hasattr(command_class, 'get_metadata'):
599
+ metadata[command_name] = command_class.get_metadata()
600
+ else:
601
+ # Fallback metadata
602
+ metadata[command_name] = {
603
+ "name": command_name,
604
+ "class": command_class.__name__,
605
+ "module": command_class.__module__,
606
+ "description": getattr(command_class, '__doc__', 'No description available')
607
+ }
608
+ except Exception as e:
609
+ logger.warning(f"Failed to get metadata for command '{command_name}': {e}")
610
+ metadata[command_name] = {
611
+ "name": command_name,
612
+ "error": f"Failed to get metadata: {str(e)}"
613
+ }
614
+
241
615
  return metadata
242
616
 
243
- def get_all_commands_info(self) -> Dict[str, Dict[str, Any]]:
617
+ def clear(self) -> None:
244
618
  """
245
- Gets information about all registered commands.
246
-
247
- Returns:
248
- Dictionary with information about all commands.
619
+ Clear all registered commands.
249
620
  """
250
- commands_info = {}
251
- # Add custom commands first (they have priority)
252
- for name in self._custom_commands:
253
- commands_info[name] = self.get_command_info(name)
254
- # Add built-in commands (custom commands will override if same name)
255
- for name in self._commands:
256
- if name not in self._custom_commands: # Only add if not overridden by custom
257
- commands_info[name] = self.get_command_info(name)
258
- return commands_info
621
+ logger.debug("Clearing all registered commands")
622
+ self._builtin_commands.clear()
623
+ self._custom_commands.clear()
624
+ self._loaded_commands.clear()
625
+ self._instances.clear()
626
+
259
627
 
260
- def discover_commands(self, package_path: Optional[str] = None) -> int:
628
+ async def reload_system(self, config_path: Optional[str] = None) -> Dict[str, Any]:
261
629
  """
262
- Automatically discovers and registers commands in the specified package.
263
- If package_path is not provided, uses configuration settings.
264
-
630
+ Universal method for system initialization and reload.
631
+ This method should be used both at startup and during reload.
632
+
265
633
  Args:
266
- package_path: Path to package with commands. If None, uses config.
634
+ config_path: Path to configuration file. If None, uses default or existing path.
267
635
 
268
636
  Returns:
269
- Number of commands discovered and registered.
637
+ Dictionary with initialization information.
270
638
  """
271
- # Get package path from config if not provided
272
- if package_path is None:
273
- try:
274
- from mcp_proxy_adapter.config import config
275
- package_path = config.get("commands.discovery_path", "mcp_proxy_adapter.commands")
276
- logger.info(f"Using commands discovery path from config: {package_path}")
277
- except Exception as e:
278
- logger.warning(f"Failed to get discovery path from config, using default: {e}")
279
- package_path = "mcp_proxy_adapter.commands"
639
+ logger.info(f"🔄 Starting system reload with config: {config_path or 'default'}")
640
+
641
+ # Step 1: Load configuration
642
+ try:
643
+ if config_path:
644
+ config.load_from_file(config_path)
645
+ logger.info(f"✅ Configuration loaded from: {config_path}")
646
+ else:
647
+ config.load_config()
648
+ logger.info("✅ Configuration loaded from default path")
649
+
650
+ config_reloaded = True
651
+ except Exception as e:
652
+ logger.error(f"❌ Failed to load configuration: {e}")
653
+ config_reloaded = False
280
654
 
281
- logger.info(f"Discovering commands in package: {package_path}")
655
+ # Step 2: Initialize logging with configuration
656
+ try:
657
+ from mcp_proxy_adapter.core.logging import setup_logging
658
+ setup_logging()
659
+ logger.info("✅ Logging initialized with configuration")
660
+ except Exception as e:
661
+ logger.error(f"❌ Failed to initialize logging: {e}")
282
662
 
283
- commands_discovered = 0
663
+ # Step 2.5: Reload protocol manager configuration
664
+ try:
665
+ from mcp_proxy_adapter.core.protocol_manager import protocol_manager
666
+ protocol_manager.reload_config()
667
+ logger.info("✅ Protocol manager configuration reloaded")
668
+ except Exception as e:
669
+ logger.error(f"❌ Failed to reload protocol manager: {e}")
670
+
671
+ # Step 3: Clear all commands (always clear for consistency)
672
+ self.clear()
284
673
 
674
+ # Step 4: Execute before init hooks
285
675
  try:
286
- package = importlib.import_module(package_path)
287
- package_dir = os.path.dirname(package.__file__ or "")
288
-
289
- for _, module_name, is_pkg in pkgutil.iter_modules([package_dir]):
290
- if is_pkg:
291
- # Recursively traverse subpackages
292
- commands_discovered += self.discover_commands(f"{package_path}.{module_name}")
293
- elif module_name.endswith("_command"):
294
- # Import only command modules
295
- module_path = f"{package_path}.{module_name}"
296
- logger.debug(f"Found command module: {module_path}")
297
-
298
- try:
299
- module = importlib.import_module(module_path)
300
-
301
- # Find all command classes in the module
302
- for name, obj in inspect.getmembers(module):
303
- if (inspect.isclass(obj) and
304
- issubclass(obj, Command) and
305
- obj != Command and
306
- not inspect.isabstract(obj)):
307
-
308
- # Get command name before registration
309
- command_name = obj.name if hasattr(obj, "name") and obj.name else obj.__name__.lower()
310
- if command_name.endswith("command"):
311
- command_name = command_name[:-7] # Remove "command" suffix
312
-
313
- # Register the command only if it doesn't exist
314
- if not self.command_exists(command_name):
315
- self.register(cast(Type[Command], obj))
316
- commands_discovered += 1
317
- logger.debug(f"Registered command: {command_name}")
318
- else:
319
- logger.debug(f"Command '{command_name}' is already registered, skipping")
320
- except ValueError as e:
321
- # Skip already registered commands
322
- logger.debug(f"Skipping command registration: {str(e)}")
323
- except Exception as e:
324
- logger.error(f"Error loading command module {module_path}: {e}")
676
+ hooks.execute_before_init_hooks()
325
677
  except Exception as e:
326
- logger.error(f"Error discovering commands: {e}")
327
-
328
- return commands_discovered
329
-
330
- def register_custom_command(self, command: Union[Type[Command], Command]) -> None:
331
- """
332
- Register a custom command with priority over built-in commands.
678
+ logger.error(f" Failed to execute before init hooks: {e}")
333
679
 
334
- Args:
335
- command: Command class or instance to register.
680
+ # Step 5: Register built-in commands
681
+ try:
682
+ from mcp_proxy_adapter.commands.builtin_commands import register_builtin_commands
683
+ builtin_commands_count = register_builtin_commands()
684
+ except Exception as e:
685
+ logger.error(f"❌ Failed to register built-in commands: {e}")
686
+ builtin_commands_count = 0
687
+
688
+ # Step 6: Execute custom commands hooks
689
+ try:
690
+ custom_commands_count = hooks.execute_custom_commands_hooks(self)
691
+ except Exception as e:
692
+ logger.error(f"❌ Failed to execute custom commands hooks: {e}")
693
+ custom_commands_count = 0
694
+
695
+ # Step 7: Load all commands (built-in, custom, loadable)
696
+ try:
697
+ load_result = self._load_all_commands()
698
+ remote_commands_count = load_result.get("remote_commands", 0)
699
+ loaded_commands_count = load_result.get("loaded_commands", 0)
700
+ except Exception as e:
701
+ logger.error(f"❌ Failed to load commands: {e}")
702
+ remote_commands_count = 0
703
+ loaded_commands_count = 0
704
+
705
+ # Step 8: Execute after init hooks
706
+ try:
707
+ hooks.execute_after_init_hooks()
708
+ except Exception as e:
709
+ logger.error(f"❌ Failed to execute after init hooks: {e}")
710
+
711
+ # Step 9: Register with proxy if enabled
712
+ proxy_registration_success = False
713
+ try:
714
+ from mcp_proxy_adapter.core.proxy_registration import register_with_proxy
336
715
 
337
- Raises:
338
- ValueError: If command with the same name is already registered.
339
- """
340
- # Determine if this is a class or an instance
341
- if isinstance(command, type) and issubclass(command, Command):
342
- command_class = command
343
- command_instance = None
344
- elif isinstance(command, Command):
345
- command_class = command.__class__
346
- command_instance = command
347
- else:
348
- raise ValueError(f"Invalid command type: {type(command)}. Expected Command class or instance.")
716
+ # Get server configuration
717
+ server_config = config.get("server", {})
718
+ server_host = server_config.get("host", "0.0.0.0")
719
+ server_port = server_config.get("port", 8000)
349
720
 
350
- # Get command name
351
- if not hasattr(command_class, "name") or not command_class.name:
352
- # Use class name if name attribute is not set
353
- command_name = command_class.__name__.lower()
354
- if command_name.endswith("command"):
355
- command_name = command_name[:-7] # Remove "command" suffix
356
- else:
357
- command_name = command_class.name
721
+ # Determine server URL based on SSL configuration
722
+ ssl_config = config.get("ssl", {})
723
+ if ssl_config.get("enabled", False):
724
+ protocol = "https"
725
+ else:
726
+ protocol = "http"
358
727
 
359
- if command_name in self._custom_commands:
360
- logger.debug(f"Custom command '{command_name}' is already registered, skipping")
361
- raise ValueError(f"Custom command '{command_name}' is already registered")
728
+ # Use localhost for external access if host is 0.0.0.0
729
+ if server_host == "0.0.0.0":
730
+ server_host = "localhost"
731
+
732
+ server_url = f"{protocol}://{server_host}:{server_port}"
362
733
 
363
- logger.debug(f"Registering custom command: {command_name}")
364
- self._custom_commands[command_name] = command_class
734
+ # Attempt proxy registration
735
+ proxy_registration_success = await register_with_proxy(server_url)
736
+ if proxy_registration_success:
737
+ logger.info("✅ Proxy registration completed successfully during system reload")
738
+ else:
739
+ logger.info("ℹ️ Proxy registration is disabled or failed during system reload")
740
+
741
+ except Exception as e:
742
+ logger.error(f"❌ Failed to register with proxy during system reload: {e}")
365
743
 
366
- # Store instance if provided
367
- if command_instance:
368
- logger.debug(f"Storing custom instance for command: {command_name}")
369
- self._instances[command_name] = command_instance
370
-
371
- def unregister_custom_command(self, command_name: str) -> None:
372
- """
373
- Remove custom command from registry.
744
+ # Get final counts
745
+ total_commands = len(self.get_all_commands())
374
746
 
375
- Args:
376
- command_name: Command name to remove.
377
-
378
- Raises:
379
- NotFoundError: If command is not found.
380
- """
381
- if command_name not in self._custom_commands:
382
- raise NotFoundError(f"Custom command '{command_name}' not found")
383
-
384
- logger.debug(f"Unregistering custom command: {command_name}")
385
- del self._custom_commands[command_name]
747
+ result = {
748
+ "config_reloaded": config_reloaded,
749
+ "builtin_commands": builtin_commands_count,
750
+ "custom_commands": custom_commands_count,
751
+ "loaded_commands": loaded_commands_count,
752
+ "remote_commands": remote_commands_count,
753
+ "total_commands": total_commands,
754
+ "proxy_registration_success": proxy_registration_success
755
+ }
386
756
 
387
- # Also remove from instances if present
388
- if command_name in self._instances:
389
- del self._instances[command_name]
757
+ logger.info(f"✅ System reload completed: {result}")
758
+ return result
390
759
 
391
- def custom_command_exists(self, command_name: str) -> bool:
760
+ def _load_all_commands(self) -> Dict[str, Any]:
392
761
  """
393
- Check if custom command exists.
762
+ Universal command loader - handles all command types.
394
763
 
395
- Args:
396
- command_name: Command name to check.
397
-
398
764
  Returns:
399
- True if custom command exists, False otherwise.
765
+ Dictionary with loading results
400
766
  """
401
- return command_name in self._custom_commands
402
-
403
- def get_custom_command(self, command_name: str) -> Type[Command]:
404
- """
405
- Get custom command class.
406
-
407
- Args:
408
- command_name: Command name.
767
+ try:
768
+ remote_commands = 0
769
+ loaded_commands = 0
409
770
 
410
- Returns:
411
- Command class.
771
+ # 1. Load commands from directory (if configured)
772
+ commands_directory = config.get("commands.commands_directory")
773
+ if commands_directory and os.path.exists(commands_directory):
774
+ logger.info(f"Loading commands from directory: {commands_directory}")
775
+ for file_path in Path(commands_directory).glob("*_command.py"):
776
+ try:
777
+ result = self.load_command_from_source(str(file_path))
778
+ if result.get("success"):
779
+ loaded_commands += result.get("commands_loaded", 0)
780
+ except Exception as e:
781
+ logger.error(f"Failed to load command from {file_path}: {e}")
412
782
 
413
- Raises:
414
- NotFoundError: If command is not found.
415
- """
416
- if command_name not in self._custom_commands:
417
- raise NotFoundError(f"Custom command '{command_name}' not found")
418
- return self._custom_commands[command_name]
419
-
420
- def get_all_custom_commands(self) -> Dict[str, Type[Command]]:
783
+ # 2. Load commands from plugin servers (if configured)
784
+ plugin_servers = config.get("commands.plugin_servers", [])
785
+ if plugin_servers:
786
+ logger.info(f"Loading commands from {len(plugin_servers)} plugin servers")
787
+ for server_url in plugin_servers:
788
+ try:
789
+ # Load catalog from server
790
+ from mcp_proxy_adapter.commands.catalog_manager import CatalogManager
791
+ catalog_manager = CatalogManager("./catalog")
792
+ server_catalog = catalog_manager.get_catalog_from_server(server_url)
793
+
794
+ # Load each command from catalog
795
+ for command_name, server_cmd in server_catalog.items():
796
+ try:
797
+ result = self.load_command_from_source(command_name)
798
+ if result.get("success"):
799
+ remote_commands += result.get("commands_loaded", 0)
800
+ except Exception as e:
801
+ logger.error(f"Failed to load command {command_name}: {e}")
802
+
803
+ except Exception as e:
804
+ logger.error(f"Failed to load from server {server_url}: {e}")
805
+
806
+ return {
807
+ "remote_commands": remote_commands,
808
+ "loaded_commands": loaded_commands
809
+ }
810
+
811
+ except Exception as e:
812
+ logger.error(f"Failed to load all commands: {e}")
813
+ return {
814
+ "remote_commands": 0,
815
+ "loaded_commands": 0,
816
+ "error": str(e)
817
+ }
818
+
819
+
820
+ def get_all_commands_info(self) -> Dict[str, Any]:
421
821
  """
422
- Get all custom commands.
822
+ Get information about all registered commands.
423
823
 
424
824
  Returns:
425
- Dictionary with custom command names as keys and classes as values.
426
- """
427
- return self._custom_commands.copy()
428
-
429
- def get_priority_command(self, command_name: str) -> Optional[Type[Command]]:
825
+ Dictionary with command information
430
826
  """
431
- Get command with priority (custom commands first, then built-in).
827
+ commands_info = {}
432
828
 
433
- Args:
434
- command_name: Command name.
435
-
436
- Returns:
437
- Command class if found, None otherwise.
438
- """
439
- # First check custom commands
440
- if command_name in self._custom_commands:
441
- return self._custom_commands[command_name]
829
+ # Get all commands
830
+ all_commands = self.get_all_commands()
442
831
 
443
- # Then check built-in commands
444
- if command_name in self._commands:
445
- return self._commands[command_name]
832
+ for command_name, command_class in all_commands.items():
833
+ try:
834
+ # Get command metadata
835
+ metadata = command_class.get_metadata()
836
+
837
+ # Get command schema
838
+ schema = command_class.get_schema()
839
+
840
+ commands_info[command_name] = {
841
+ "name": command_name,
842
+ "metadata": metadata,
843
+ "schema": schema,
844
+ "type": self._get_command_type(command_name)
845
+ }
846
+
847
+ except Exception as e:
848
+ logger.warning(f"Failed to get info for command {command_name}: {e}")
849
+ commands_info[command_name] = {
850
+ "name": command_name,
851
+ "error": str(e),
852
+ "type": self._get_command_type(command_name)
853
+ }
446
854
 
447
- return None
855
+ return {
856
+ "commands": commands_info,
857
+ "total": len(commands_info)
858
+ }
448
859
 
449
- def command_exists_with_priority(self, command_name: str) -> bool:
860
+ def get_command_info(self, command_name: str) -> Optional[Dict[str, Any]]:
450
861
  """
451
- Check if command exists (custom or built-in).
862
+ Get information about a specific command.
452
863
 
453
864
  Args:
454
- command_name: Command name to check.
865
+ command_name: Name of the command
455
866
 
456
867
  Returns:
457
- True if command exists, False otherwise.
868
+ Dictionary with command information or None if not found
458
869
  """
459
- return (command_name in self._custom_commands or
460
- command_name in self._commands)
461
-
462
- def get_command_with_priority(self, command_name: str) -> Type[Command]:
463
- """
464
- Get command with priority (custom commands first, then built-in).
465
-
466
- Args:
467
- command_name: Command name.
870
+ try:
871
+ # Check if command exists
872
+ if not self.command_exists(command_name):
873
+ return None
468
874
 
469
- Returns:
470
- Command class.
875
+ # Get command class
876
+ command_class = self.get_command(command_name)
471
877
 
472
- Raises:
473
- NotFoundError: If command is not found.
474
- """
475
- # First check custom commands
476
- if command_name in self._custom_commands:
477
- return self._custom_commands[command_name]
478
-
479
- # Then check built-in commands
480
- if command_name in self._commands:
481
- return self._commands[command_name]
482
-
483
- raise NotFoundError(f"Command '{command_name}' not found")
484
-
485
- def clear(self) -> None:
486
- """
487
- Clear all registered commands.
488
- """
489
- logger.debug("Clearing all registered commands")
490
- self._commands.clear()
491
- self._instances.clear()
492
- self._custom_commands.clear()
878
+ # Get command metadata
879
+ metadata = command_class.get_metadata()
880
+
881
+ # Get command schema
882
+ schema = command_class.get_schema()
883
+
884
+ return {
885
+ "name": command_name,
886
+ "metadata": metadata,
887
+ "schema": schema,
888
+ "type": self._get_command_type(command_name)
889
+ }
890
+
891
+ except Exception as e:
892
+ logger.warning(f"Failed to get info for command {command_name}: {e}")
893
+ return {
894
+ "name": command_name,
895
+ "error": str(e),
896
+ "type": self._get_command_type(command_name)
897
+ }
493
898
 
494
- def reload_config_and_commands(self, package_path: Optional[str] = None) -> Dict[str, Any]:
899
+ def _get_command_type(self, command_name: str) -> str:
495
900
  """
496
- Reload configuration and rediscover commands.
901
+ Get the type of a command (built-in, custom, or loaded).
497
902
 
498
903
  Args:
499
- package_path: Path to package with commands. If None, uses config.
904
+ command_name: Name of the command
500
905
 
501
906
  Returns:
502
- Dictionary with reload information including:
503
- - config_reloaded: Whether config was reloaded
504
- - commands_discovered: Number of commands discovered
505
- - custom_commands_preserved: Number of custom commands preserved
506
- - total_commands: Total number of commands after reload
907
+ Command type string
507
908
  """
508
- logger.info("🔄 Starting configuration and commands reload...")
509
-
510
- # Store current custom commands
511
- custom_commands_backup = self._custom_commands.copy()
512
-
513
- # Reload configuration
514
- try:
515
- from mcp_proxy_adapter.config import config
516
- config.load_config()
517
- config_reloaded = True
518
- logger.info("✅ Configuration reloaded successfully")
519
- except Exception as e:
520
- logger.error(f"❌ Failed to reload configuration: {e}")
521
- config_reloaded = False
522
-
523
- # Reinitialize logging with new configuration
524
- try:
525
- from mcp_proxy_adapter.core.logging import setup_logging
526
- setup_logging()
527
- logger.info("✅ Logging reinitialized with new configuration")
528
- except Exception as e:
529
- logger.error(f"❌ Failed to reinitialize logging: {e}")
530
-
531
- # Clear all commands except custom ones
532
- self._commands.clear()
533
- self._instances.clear()
534
-
535
- # Restore custom commands
536
- self._custom_commands = custom_commands_backup
537
- custom_commands_preserved = len(custom_commands_backup)
538
-
539
- # Rediscover commands
540
- try:
541
- commands_discovered = self.discover_commands(package_path)
542
- logger.info(f"✅ Rediscovered {commands_discovered} commands")
543
- except Exception as e:
544
- logger.error(f"❌ Failed to rediscover commands: {e}")
545
- commands_discovered = 0
546
-
547
- # Get final counts
548
- total_commands = len(self._commands)
549
- built_in_commands = total_commands - custom_commands_preserved
550
- custom_commands = custom_commands_preserved
551
-
552
- result = {
553
- "config_reloaded": config_reloaded,
554
- "commands_discovered": commands_discovered,
555
- "custom_commands_preserved": custom_commands_preserved,
556
- "total_commands": total_commands,
557
- "built_in_commands": built_in_commands,
558
- "custom_commands": custom_commands
559
- }
560
-
561
- logger.info(f"🔄 Reload completed: {result}")
562
- return result
909
+ if command_name in self._custom_commands:
910
+ return "custom"
911
+ elif command_name in self._builtin_commands:
912
+ return "built-in"
913
+ elif command_name in self._loaded_commands:
914
+ return "loaded"
915
+ else:
916
+ return "unknown"
563
917
 
564
918
 
565
919
  # Global command registry instance
566
920
  registry = CommandRegistry()
921
+
922
+ # Remove automatic command discovery - use reload_system instead
923
+ # This prevents duplication of loading logic