mcp-proxy-adapter 4.1.0__py3-none-any.whl → 6.0.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.
- mcp_proxy_adapter/__main__.py +12 -0
- mcp_proxy_adapter/api/app.py +138 -11
- mcp_proxy_adapter/api/handlers.py +16 -1
- mcp_proxy_adapter/api/middleware/__init__.py +30 -29
- mcp_proxy_adapter/api/middleware/auth_adapter.py +235 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
- mcp_proxy_adapter/api/middleware/factory.py +219 -0
- mcp_proxy_adapter/api/middleware/logging.py +32 -6
- mcp_proxy_adapter/api/middleware/mtls_adapter.py +305 -0
- mcp_proxy_adapter/api/middleware/mtls_middleware.py +296 -0
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
- mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +241 -0
- mcp_proxy_adapter/api/middleware/roles_adapter.py +365 -0
- mcp_proxy_adapter/api/middleware/roles_middleware.py +381 -0
- mcp_proxy_adapter/api/middleware/security.py +376 -0
- mcp_proxy_adapter/api/middleware/token_auth_middleware.py +261 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
- mcp_proxy_adapter/commands/__init__.py +13 -4
- mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
- mcp_proxy_adapter/commands/base.py +61 -30
- mcp_proxy_adapter/commands/builtin_commands.py +89 -0
- mcp_proxy_adapter/commands/catalog_manager.py +838 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
- mcp_proxy_adapter/commands/command_registry.py +705 -345
- mcp_proxy_adapter/commands/dependency_manager.py +245 -0
- mcp_proxy_adapter/commands/health_command.py +7 -0
- mcp_proxy_adapter/commands/hooks.py +200 -167
- mcp_proxy_adapter/commands/key_management_command.py +506 -0
- mcp_proxy_adapter/commands/load_command.py +176 -0
- mcp_proxy_adapter/commands/plugins_command.py +235 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +268 -0
- mcp_proxy_adapter/commands/reload_command.py +48 -50
- mcp_proxy_adapter/commands/result.py +1 -0
- mcp_proxy_adapter/commands/roles_management_command.py +697 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
- mcp_proxy_adapter/commands/token_management_command.py +529 -0
- mcp_proxy_adapter/commands/transport_management_command.py +144 -0
- mcp_proxy_adapter/commands/unload_command.py +158 -0
- mcp_proxy_adapter/config.py +99 -2
- mcp_proxy_adapter/core/auth_validator.py +606 -0
- mcp_proxy_adapter/core/certificate_utils.py +827 -0
- mcp_proxy_adapter/core/config_converter.py +405 -0
- mcp_proxy_adapter/core/config_validator.py +218 -0
- mcp_proxy_adapter/core/logging.py +11 -0
- mcp_proxy_adapter/core/protocol_manager.py +226 -0
- mcp_proxy_adapter/core/proxy_registration.py +270 -0
- mcp_proxy_adapter/core/role_utils.py +426 -0
- mcp_proxy_adapter/core/security_adapter.py +373 -0
- mcp_proxy_adapter/core/security_factory.py +239 -0
- mcp_proxy_adapter/core/settings.py +1 -0
- mcp_proxy_adapter/core/ssl_utils.py +233 -0
- mcp_proxy_adapter/core/transport_manager.py +292 -0
- mcp_proxy_adapter/custom_openapi.py +22 -11
- mcp_proxy_adapter/examples/basic_server/config.json +58 -23
- mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +54 -0
- mcp_proxy_adapter/examples/basic_server/config_http.json +70 -0
- mcp_proxy_adapter/examples/basic_server/config_http_only.json +52 -0
- mcp_proxy_adapter/examples/basic_server/config_https.json +58 -0
- mcp_proxy_adapter/examples/basic_server/config_mtls.json +58 -0
- mcp_proxy_adapter/examples/basic_server/config_ssl.json +46 -0
- mcp_proxy_adapter/examples/basic_server/server.py +17 -1
- mcp_proxy_adapter/examples/custom_commands/__init__.py +1 -1
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +339 -23
- mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +105 -0
- mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +129 -0
- mcp_proxy_adapter/examples/custom_commands/config.json +97 -41
- mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +46 -0
- mcp_proxy_adapter/examples/custom_commands/config_https_only.json +46 -0
- mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +33 -0
- mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +46 -0
- mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +33 -0
- mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +33 -0
- mcp_proxy_adapter/examples/custom_commands/full_help_response.json +1 -0
- mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +629 -0
- mcp_proxy_adapter/examples/custom_commands/get_openapi.py +103 -0
- mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +129 -0
- mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +278 -0
- mcp_proxy_adapter/examples/custom_commands/server.py +92 -63
- mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +75 -0
- mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +299 -0
- mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +278 -0
- mcp_proxy_adapter/examples/custom_commands/test_openapi.py +27 -0
- mcp_proxy_adapter/examples/custom_commands/test_registry.py +23 -0
- mcp_proxy_adapter/examples/custom_commands/test_simple.py +19 -0
- mcp_proxy_adapter/examples/custom_project_example/README.md +103 -0
- mcp_proxy_adapter/examples/custom_project_example/README_EN.md +103 -0
- mcp_proxy_adapter/examples/simple_custom_commands/README.md +149 -0
- mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +149 -0
- mcp_proxy_adapter/main.py +175 -0
- mcp_proxy_adapter/schemas/roles_schema.json +162 -0
- mcp_proxy_adapter/tests/unit/test_config.py +53 -0
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/METADATA +2 -1
- mcp_proxy_adapter-6.0.0.dist-info/RECORD +179 -0
- mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
- mcp_proxy_adapter-4.1.0.dist-info/RECORD +0 -110
- {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.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,17 +53,108 @@ class CommandRegistry:
|
|
39
53
|
"""
|
40
54
|
Initialize command registry.
|
41
55
|
"""
|
42
|
-
self.
|
43
|
-
self.
|
44
|
-
self.
|
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")
|
45
85
|
|
46
|
-
def
|
86
|
+
def register_custom(self, command: Union[Type[Command], Command]) -> None:
|
47
87
|
"""
|
48
|
-
|
88
|
+
Register a custom command with highest priority.
|
49
89
|
|
50
90
|
Args:
|
51
91
|
command: Command class or instance to register.
|
52
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
|
146
|
+
|
147
|
+
def _register_command(self, command: Union[Type[Command], Command],
|
148
|
+
target_dict: Dict[str, Type[Command]],
|
149
|
+
command_type: str) -> None:
|
150
|
+
"""
|
151
|
+
Internal method to register a command in the specified dictionary.
|
152
|
+
|
153
|
+
Args:
|
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.
|
157
|
+
|
53
158
|
Raises:
|
54
159
|
ValueError: If command with the same name is already registered.
|
55
160
|
"""
|
@@ -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
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
78
237
|
|
79
|
-
|
80
|
-
|
238
|
+
Returns:
|
239
|
+
Dictionary with loading result information
|
240
|
+
"""
|
241
|
+
try:
|
242
|
+
from mcp_proxy_adapter.commands.catalog_manager import CatalogManager
|
243
|
+
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
335
|
+
def _load_command_from_file(self, file_path: str, is_temporary: bool = False) -> Dict[str, Any]:
|
88
336
|
"""
|
89
|
-
|
90
|
-
|
337
|
+
Load command from local file.
|
338
|
+
|
91
339
|
Args:
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
98
|
-
|
424
|
+
Unload a loaded command from registry.
|
425
|
+
|
426
|
+
Args:
|
427
|
+
command_name: Name of the command to unload
|
99
428
|
|
100
|
-
|
101
|
-
|
429
|
+
Returns:
|
430
|
+
Dictionary with unloading result information
|
431
|
+
"""
|
432
|
+
logger.info(f"Unloading command: {command_name}")
|
102
433
|
|
103
|
-
#
|
104
|
-
if command_name in self.
|
105
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
158
|
-
# (this will raise error if command requires dependencies)
|
525
|
+
# Otherwise create new instance
|
159
526
|
try:
|
160
|
-
command_class = self.
|
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,391 +532,384 @@ class CommandRegistry:
|
|
165
532
|
|
166
533
|
def has_instance(self, command_name: str) -> bool:
|
167
534
|
"""
|
168
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
570
|
+
def get_commands_by_type(self) -> Dict[str, Dict[str, Type[Command]]]:
|
188
571
|
"""
|
189
|
-
|
190
|
-
|
191
|
-
Args:
|
192
|
-
command_name: Command name.
|
572
|
+
Get commands grouped by type.
|
193
573
|
|
194
574
|
Returns:
|
195
|
-
Dictionary with
|
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
|
-
"
|
204
|
-
"
|
205
|
-
"
|
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
|
-
|
588
|
+
Dictionary with command names as keys and metadata as values.
|
232
589
|
"""
|
233
590
|
metadata = {}
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
for
|
239
|
-
|
240
|
-
|
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
|
617
|
+
def clear(self) -> None:
|
244
618
|
"""
|
245
|
-
|
246
|
-
|
247
|
-
Returns:
|
248
|
-
Dictionary with information about all commands.
|
619
|
+
Clear all registered commands.
|
249
620
|
"""
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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
|
628
|
+
async def reload_system(self, config_path: Optional[str] = None) -> Dict[str, Any]:
|
261
629
|
"""
|
262
|
-
|
263
|
-
|
630
|
+
Universal method for system initialization and reload.
|
631
|
+
This method should be used both at startup and during reload.
|
632
|
+
|
264
633
|
Args:
|
265
|
-
|
634
|
+
config_path: Path to configuration file. If None, uses default or existing path.
|
266
635
|
|
267
636
|
Returns:
|
268
|
-
|
637
|
+
Dictionary with initialization information.
|
269
638
|
"""
|
270
|
-
logger.info(f"
|
639
|
+
logger.info(f"🔄 Starting system reload with config: {config_path or 'default'}")
|
271
640
|
|
272
|
-
|
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
|
273
654
|
|
655
|
+
# Step 2: Initialize logging with configuration
|
274
656
|
try:
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
for _, module_name, is_pkg in pkgutil.iter_modules([package_dir]):
|
279
|
-
if is_pkg:
|
280
|
-
# Recursively traverse subpackages
|
281
|
-
commands_discovered += self.discover_commands(f"{package_path}.{module_name}")
|
282
|
-
elif module_name.endswith("_command"):
|
283
|
-
# Import only command modules
|
284
|
-
module_path = f"{package_path}.{module_name}"
|
285
|
-
logger.debug(f"Found command module: {module_path}")
|
286
|
-
|
287
|
-
try:
|
288
|
-
module = importlib.import_module(module_path)
|
289
|
-
|
290
|
-
# Find all command classes in the module
|
291
|
-
for name, obj in inspect.getmembers(module):
|
292
|
-
if (inspect.isclass(obj) and
|
293
|
-
issubclass(obj, Command) and
|
294
|
-
obj != Command and
|
295
|
-
not inspect.isabstract(obj)):
|
296
|
-
|
297
|
-
# Get command name before registration
|
298
|
-
command_name = obj.name if hasattr(obj, "name") and obj.name else obj.__name__.lower()
|
299
|
-
if command_name.endswith("command"):
|
300
|
-
command_name = command_name[:-7] # Remove "command" suffix
|
301
|
-
|
302
|
-
# Register the command only if it doesn't exist
|
303
|
-
if not self.command_exists(command_name):
|
304
|
-
self.register(cast(Type[Command], obj))
|
305
|
-
commands_discovered += 1
|
306
|
-
logger.debug(f"Registered command: {command_name}")
|
307
|
-
else:
|
308
|
-
logger.debug(f"Command '{command_name}' is already registered, skipping")
|
309
|
-
except ValueError as e:
|
310
|
-
# Skip already registered commands
|
311
|
-
logger.debug(f"Skipping command registration: {str(e)}")
|
312
|
-
except Exception as e:
|
313
|
-
logger.error(f"Error loading command module {module_path}: {e}")
|
657
|
+
from mcp_proxy_adapter.core.logging import setup_logging
|
658
|
+
setup_logging()
|
659
|
+
logger.info("✅ Logging initialized with configuration")
|
314
660
|
except Exception as e:
|
315
|
-
logger.error(f"
|
316
|
-
|
317
|
-
return commands_discovered
|
318
|
-
|
319
|
-
def register_custom_command(self, command: Union[Type[Command], Command]) -> None:
|
320
|
-
"""
|
321
|
-
Register a custom command with priority over built-in commands.
|
661
|
+
logger.error(f"❌ Failed to initialize logging: {e}")
|
322
662
|
|
323
|
-
|
324
|
-
|
663
|
+
# Step 3: Clear all commands (always clear for consistency)
|
664
|
+
self.clear()
|
665
|
+
|
666
|
+
# Step 4: Execute before init hooks
|
667
|
+
try:
|
668
|
+
hooks.execute_before_init_hooks()
|
669
|
+
except Exception as e:
|
670
|
+
logger.error(f"❌ Failed to execute before init hooks: {e}")
|
671
|
+
|
672
|
+
# Step 5: Register built-in commands
|
673
|
+
try:
|
674
|
+
from mcp_proxy_adapter.commands.builtin_commands import register_builtin_commands
|
675
|
+
builtin_commands_count = register_builtin_commands()
|
676
|
+
except Exception as e:
|
677
|
+
logger.error(f"❌ Failed to register built-in commands: {e}")
|
678
|
+
builtin_commands_count = 0
|
679
|
+
|
680
|
+
# Step 6: Execute custom commands hooks
|
681
|
+
try:
|
682
|
+
custom_commands_count = hooks.execute_custom_commands_hooks(self)
|
683
|
+
except Exception as e:
|
684
|
+
logger.error(f"❌ Failed to execute custom commands hooks: {e}")
|
685
|
+
custom_commands_count = 0
|
686
|
+
|
687
|
+
# Step 7: Load all commands (built-in, custom, loadable)
|
688
|
+
try:
|
689
|
+
load_result = self._load_all_commands()
|
690
|
+
remote_commands_count = load_result.get("remote_commands", 0)
|
691
|
+
loaded_commands_count = load_result.get("loaded_commands", 0)
|
692
|
+
except Exception as e:
|
693
|
+
logger.error(f"❌ Failed to load commands: {e}")
|
694
|
+
remote_commands_count = 0
|
695
|
+
loaded_commands_count = 0
|
696
|
+
|
697
|
+
# Step 8: Execute after init hooks
|
698
|
+
try:
|
699
|
+
hooks.execute_after_init_hooks()
|
700
|
+
except Exception as e:
|
701
|
+
logger.error(f"❌ Failed to execute after init hooks: {e}")
|
702
|
+
|
703
|
+
# Step 9: Register with proxy if enabled
|
704
|
+
proxy_registration_success = False
|
705
|
+
try:
|
706
|
+
from mcp_proxy_adapter.core.proxy_registration import register_with_proxy
|
325
707
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
if isinstance(command, type) and issubclass(command, Command):
|
331
|
-
command_class = command
|
332
|
-
command_instance = None
|
333
|
-
elif isinstance(command, Command):
|
334
|
-
command_class = command.__class__
|
335
|
-
command_instance = command
|
336
|
-
else:
|
337
|
-
raise ValueError(f"Invalid command type: {type(command)}. Expected Command class or instance.")
|
708
|
+
# Get server configuration
|
709
|
+
server_config = config.get("server", {})
|
710
|
+
server_host = server_config.get("host", "0.0.0.0")
|
711
|
+
server_port = server_config.get("port", 8000)
|
338
712
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
else:
|
346
|
-
command_name = command_class.name
|
713
|
+
# Determine server URL based on SSL configuration
|
714
|
+
ssl_config = config.get("ssl", {})
|
715
|
+
if ssl_config.get("enabled", False):
|
716
|
+
protocol = "https"
|
717
|
+
else:
|
718
|
+
protocol = "http"
|
347
719
|
|
348
|
-
|
349
|
-
|
350
|
-
|
720
|
+
# Use localhost for external access if host is 0.0.0.0
|
721
|
+
if server_host == "0.0.0.0":
|
722
|
+
server_host = "localhost"
|
723
|
+
|
724
|
+
server_url = f"{protocol}://{server_host}:{server_port}"
|
351
725
|
|
352
|
-
|
353
|
-
|
726
|
+
# Attempt proxy registration
|
727
|
+
proxy_registration_success = await register_with_proxy(server_url)
|
728
|
+
if proxy_registration_success:
|
729
|
+
logger.info("✅ Proxy registration completed successfully during system reload")
|
730
|
+
else:
|
731
|
+
logger.info("ℹ️ Proxy registration is disabled or failed during system reload")
|
732
|
+
|
733
|
+
except Exception as e:
|
734
|
+
logger.error(f"❌ Failed to register with proxy during system reload: {e}")
|
354
735
|
|
355
|
-
#
|
356
|
-
|
357
|
-
logger.debug(f"Storing custom instance for command: {command_name}")
|
358
|
-
self._instances[command_name] = command_instance
|
359
|
-
|
360
|
-
def unregister_custom_command(self, command_name: str) -> None:
|
361
|
-
"""
|
362
|
-
Remove custom command from registry.
|
736
|
+
# Get final counts
|
737
|
+
total_commands = len(self.get_all_commands())
|
363
738
|
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
logger.debug(f"Unregistering custom command: {command_name}")
|
374
|
-
del self._custom_commands[command_name]
|
739
|
+
result = {
|
740
|
+
"config_reloaded": config_reloaded,
|
741
|
+
"builtin_commands": builtin_commands_count,
|
742
|
+
"custom_commands": custom_commands_count,
|
743
|
+
"loaded_commands": loaded_commands_count,
|
744
|
+
"remote_commands": remote_commands_count,
|
745
|
+
"total_commands": total_commands,
|
746
|
+
"proxy_registration_success": proxy_registration_success
|
747
|
+
}
|
375
748
|
|
376
|
-
|
377
|
-
|
378
|
-
del self._instances[command_name]
|
749
|
+
logger.info(f"✅ System reload completed: {result}")
|
750
|
+
return result
|
379
751
|
|
380
|
-
def
|
752
|
+
def _load_all_commands(self) -> Dict[str, Any]:
|
381
753
|
"""
|
382
|
-
|
754
|
+
Universal command loader - handles all command types.
|
383
755
|
|
384
|
-
Args:
|
385
|
-
command_name: Command name to check.
|
386
|
-
|
387
756
|
Returns:
|
388
|
-
|
757
|
+
Dictionary with loading results
|
389
758
|
"""
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
"""
|
394
|
-
Get custom command class.
|
395
|
-
|
396
|
-
Args:
|
397
|
-
command_name: Command name.
|
759
|
+
try:
|
760
|
+
remote_commands = 0
|
761
|
+
loaded_commands = 0
|
398
762
|
|
399
|
-
|
400
|
-
|
763
|
+
# 1. Load commands from directory (if configured)
|
764
|
+
commands_directory = config.get("commands.commands_directory")
|
765
|
+
if commands_directory and os.path.exists(commands_directory):
|
766
|
+
logger.info(f"Loading commands from directory: {commands_directory}")
|
767
|
+
for file_path in Path(commands_directory).glob("*_command.py"):
|
768
|
+
try:
|
769
|
+
result = self.load_command_from_source(str(file_path))
|
770
|
+
if result.get("success"):
|
771
|
+
loaded_commands += result.get("commands_loaded", 0)
|
772
|
+
except Exception as e:
|
773
|
+
logger.error(f"Failed to load command from {file_path}: {e}")
|
401
774
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
775
|
+
# 2. Load commands from plugin servers (if configured)
|
776
|
+
plugin_servers = config.get("commands.plugin_servers", [])
|
777
|
+
if plugin_servers:
|
778
|
+
logger.info(f"Loading commands from {len(plugin_servers)} plugin servers")
|
779
|
+
for server_url in plugin_servers:
|
780
|
+
try:
|
781
|
+
# Load catalog from server
|
782
|
+
from mcp_proxy_adapter.commands.catalog_manager import CatalogManager
|
783
|
+
catalog_manager = CatalogManager("./catalog")
|
784
|
+
server_catalog = catalog_manager.get_catalog_from_server(server_url)
|
785
|
+
|
786
|
+
# Load each command from catalog
|
787
|
+
for command_name, server_cmd in server_catalog.items():
|
788
|
+
try:
|
789
|
+
result = self.load_command_from_source(command_name)
|
790
|
+
if result.get("success"):
|
791
|
+
remote_commands += result.get("commands_loaded", 0)
|
792
|
+
except Exception as e:
|
793
|
+
logger.error(f"Failed to load command {command_name}: {e}")
|
794
|
+
|
795
|
+
except Exception as e:
|
796
|
+
logger.error(f"Failed to load from server {server_url}: {e}")
|
797
|
+
|
798
|
+
return {
|
799
|
+
"remote_commands": remote_commands,
|
800
|
+
"loaded_commands": loaded_commands
|
801
|
+
}
|
802
|
+
|
803
|
+
except Exception as e:
|
804
|
+
logger.error(f"Failed to load all commands: {e}")
|
805
|
+
return {
|
806
|
+
"remote_commands": 0,
|
807
|
+
"loaded_commands": 0,
|
808
|
+
"error": str(e)
|
809
|
+
}
|
810
|
+
|
811
|
+
|
812
|
+
def get_all_commands_info(self) -> Dict[str, Any]:
|
419
813
|
"""
|
420
|
-
Get
|
814
|
+
Get information about all registered commands.
|
421
815
|
|
422
|
-
Args:
|
423
|
-
command_name: Command name.
|
424
|
-
|
425
816
|
Returns:
|
426
|
-
|
817
|
+
Dictionary with command information
|
427
818
|
"""
|
428
|
-
|
429
|
-
if command_name in self._custom_commands:
|
430
|
-
return self._custom_commands[command_name]
|
819
|
+
commands_info = {}
|
431
820
|
|
432
|
-
#
|
433
|
-
|
434
|
-
|
821
|
+
# Get all commands
|
822
|
+
all_commands = self.get_all_commands()
|
823
|
+
|
824
|
+
for command_name, command_class in all_commands.items():
|
825
|
+
try:
|
826
|
+
# Get command metadata
|
827
|
+
metadata = command_class.get_metadata()
|
828
|
+
|
829
|
+
# Get command schema
|
830
|
+
schema = command_class.get_schema()
|
831
|
+
|
832
|
+
commands_info[command_name] = {
|
833
|
+
"name": command_name,
|
834
|
+
"metadata": metadata,
|
835
|
+
"schema": schema,
|
836
|
+
"type": self._get_command_type(command_name)
|
837
|
+
}
|
838
|
+
|
839
|
+
except Exception as e:
|
840
|
+
logger.warning(f"Failed to get info for command {command_name}: {e}")
|
841
|
+
commands_info[command_name] = {
|
842
|
+
"name": command_name,
|
843
|
+
"error": str(e),
|
844
|
+
"type": self._get_command_type(command_name)
|
845
|
+
}
|
435
846
|
|
436
|
-
return
|
847
|
+
return {
|
848
|
+
"commands": commands_info,
|
849
|
+
"total": len(commands_info)
|
850
|
+
}
|
437
851
|
|
438
|
-
def
|
852
|
+
def get_command_info(self, command_name: str) -> Optional[Dict[str, Any]]:
|
439
853
|
"""
|
440
|
-
|
854
|
+
Get information about a specific command.
|
441
855
|
|
442
856
|
Args:
|
443
|
-
command_name:
|
857
|
+
command_name: Name of the command
|
444
858
|
|
445
859
|
Returns:
|
446
|
-
|
860
|
+
Dictionary with command information or None if not found
|
447
861
|
"""
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
"""
|
453
|
-
Get command with priority (custom commands first, then built-in).
|
454
|
-
|
455
|
-
Args:
|
456
|
-
command_name: Command name.
|
862
|
+
try:
|
863
|
+
# Check if command exists
|
864
|
+
if not self.command_exists(command_name):
|
865
|
+
return None
|
457
866
|
|
458
|
-
|
459
|
-
|
867
|
+
# Get command class
|
868
|
+
command_class = self.get_command(command_name)
|
460
869
|
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
self._custom_commands.clear()
|
870
|
+
# Get command metadata
|
871
|
+
metadata = command_class.get_metadata()
|
872
|
+
|
873
|
+
# Get command schema
|
874
|
+
schema = command_class.get_schema()
|
875
|
+
|
876
|
+
return {
|
877
|
+
"name": command_name,
|
878
|
+
"metadata": metadata,
|
879
|
+
"schema": schema,
|
880
|
+
"type": self._get_command_type(command_name)
|
881
|
+
}
|
882
|
+
|
883
|
+
except Exception as e:
|
884
|
+
logger.warning(f"Failed to get info for command {command_name}: {e}")
|
885
|
+
return {
|
886
|
+
"name": command_name,
|
887
|
+
"error": str(e),
|
888
|
+
"type": self._get_command_type(command_name)
|
889
|
+
}
|
482
890
|
|
483
|
-
def
|
891
|
+
def _get_command_type(self, command_name: str) -> str:
|
484
892
|
"""
|
485
|
-
|
893
|
+
Get the type of a command (built-in, custom, or loaded).
|
486
894
|
|
487
895
|
Args:
|
488
|
-
|
896
|
+
command_name: Name of the command
|
489
897
|
|
490
898
|
Returns:
|
491
|
-
|
492
|
-
- config_reloaded: Whether config was reloaded
|
493
|
-
- commands_discovered: Number of commands discovered
|
494
|
-
- custom_commands_preserved: Number of custom commands preserved
|
495
|
-
- total_commands: Total number of commands after reload
|
899
|
+
Command type string
|
496
900
|
"""
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
config.load_config()
|
506
|
-
config_reloaded = True
|
507
|
-
logger.info("✅ Configuration reloaded successfully")
|
508
|
-
except Exception as e:
|
509
|
-
logger.error(f"❌ Failed to reload configuration: {e}")
|
510
|
-
config_reloaded = False
|
511
|
-
|
512
|
-
# Reinitialize logging with new configuration
|
513
|
-
try:
|
514
|
-
from mcp_proxy_adapter.core.logging import setup_logging
|
515
|
-
setup_logging()
|
516
|
-
logger.info("✅ Logging reinitialized with new configuration")
|
517
|
-
except Exception as e:
|
518
|
-
logger.error(f"❌ Failed to reinitialize logging: {e}")
|
519
|
-
|
520
|
-
# Clear all commands except custom ones
|
521
|
-
self._commands.clear()
|
522
|
-
self._instances.clear()
|
523
|
-
|
524
|
-
# Restore custom commands
|
525
|
-
self._custom_commands = custom_commands_backup
|
526
|
-
custom_commands_preserved = len(custom_commands_backup)
|
527
|
-
|
528
|
-
# Rediscover commands
|
529
|
-
try:
|
530
|
-
commands_discovered = self.discover_commands(package_path)
|
531
|
-
logger.info(f"✅ Rediscovered {commands_discovered} commands")
|
532
|
-
except Exception as e:
|
533
|
-
logger.error(f"❌ Failed to rediscover commands: {e}")
|
534
|
-
commands_discovered = 0
|
535
|
-
|
536
|
-
# Get final counts
|
537
|
-
total_commands = len(self._commands)
|
538
|
-
built_in_commands = total_commands - custom_commands_preserved
|
539
|
-
custom_commands = custom_commands_preserved
|
540
|
-
|
541
|
-
result = {
|
542
|
-
"config_reloaded": config_reloaded,
|
543
|
-
"commands_discovered": commands_discovered,
|
544
|
-
"custom_commands_preserved": custom_commands_preserved,
|
545
|
-
"total_commands": total_commands,
|
546
|
-
"built_in_commands": built_in_commands,
|
547
|
-
"custom_commands": custom_commands
|
548
|
-
}
|
549
|
-
|
550
|
-
logger.info(f"🔄 Reload completed: {result}")
|
551
|
-
return result
|
901
|
+
if command_name in self._custom_commands:
|
902
|
+
return "custom"
|
903
|
+
elif command_name in self._builtin_commands:
|
904
|
+
return "built-in"
|
905
|
+
elif command_name in self._loaded_commands:
|
906
|
+
return "loaded"
|
907
|
+
else:
|
908
|
+
return "unknown"
|
552
909
|
|
553
910
|
|
554
911
|
# Global command registry instance
|
555
912
|
registry = CommandRegistry()
|
913
|
+
|
914
|
+
# Remove automatic command discovery - use reload_system instead
|
915
|
+
# This prevents duplication of loading logic
|