mcp-proxy-adapter 6.9.28__py3-none-any.whl → 6.9.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcp-proxy-adapter might be problematic. Click here for more details.

Files changed (212) hide show
  1. mcp_proxy_adapter/__init__.py +10 -0
  2. mcp_proxy_adapter/__main__.py +8 -21
  3. mcp_proxy_adapter/api/app.py +10 -913
  4. mcp_proxy_adapter/api/core/__init__.py +18 -0
  5. mcp_proxy_adapter/api/core/app_factory.py +243 -0
  6. mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
  7. mcp_proxy_adapter/api/core/registration_manager.py +166 -0
  8. mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
  9. mcp_proxy_adapter/api/handlers.py +78 -199
  10. mcp_proxy_adapter/api/middleware/__init__.py +1 -44
  11. mcp_proxy_adapter/api/middleware/base.py +0 -42
  12. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +0 -85
  13. mcp_proxy_adapter/api/middleware/error_handling.py +1 -127
  14. mcp_proxy_adapter/api/middleware/factory.py +0 -94
  15. mcp_proxy_adapter/api/middleware/logging.py +0 -112
  16. mcp_proxy_adapter/api/middleware/performance.py +0 -35
  17. mcp_proxy_adapter/api/middleware/protocol_middleware.py +2 -98
  18. mcp_proxy_adapter/api/middleware/transport_middleware.py +0 -37
  19. mcp_proxy_adapter/api/middleware/unified_security.py +10 -10
  20. mcp_proxy_adapter/api/middleware/user_info_middleware.py +0 -118
  21. mcp_proxy_adapter/api/openapi/__init__.py +21 -0
  22. mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
  23. mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
  24. mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
  25. mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
  26. mcp_proxy_adapter/api/schemas.py +0 -61
  27. mcp_proxy_adapter/api/tool_integration.py +0 -117
  28. mcp_proxy_adapter/api/tools.py +0 -46
  29. mcp_proxy_adapter/cli/__init__.py +12 -0
  30. mcp_proxy_adapter/cli/commands/__init__.py +15 -0
  31. mcp_proxy_adapter/cli/commands/client.py +100 -0
  32. mcp_proxy_adapter/cli/commands/config_generate.py +21 -0
  33. mcp_proxy_adapter/cli/commands/config_validate.py +36 -0
  34. mcp_proxy_adapter/cli/commands/generate.py +259 -0
  35. mcp_proxy_adapter/cli/commands/server.py +174 -0
  36. mcp_proxy_adapter/cli/commands/sets.py +128 -0
  37. mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
  38. mcp_proxy_adapter/cli/examples/__init__.py +8 -0
  39. mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
  40. mcp_proxy_adapter/cli/examples/https_token.py +96 -0
  41. mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
  42. mcp_proxy_adapter/cli/main.py +63 -0
  43. mcp_proxy_adapter/cli/parser.py +324 -0
  44. mcp_proxy_adapter/cli/validators.py +231 -0
  45. mcp_proxy_adapter/client/jsonrpc_client.py +406 -0
  46. mcp_proxy_adapter/client/proxy.py +45 -0
  47. mcp_proxy_adapter/commands/__init__.py +44 -28
  48. mcp_proxy_adapter/commands/auth_validation_command.py +7 -344
  49. mcp_proxy_adapter/commands/base.py +19 -43
  50. mcp_proxy_adapter/commands/builtin_commands.py +0 -75
  51. mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
  52. mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
  53. mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
  54. mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
  55. mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
  56. mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
  57. mcp_proxy_adapter/commands/catalog_manager.py +58 -928
  58. mcp_proxy_adapter/commands/cert_monitor_command.py +0 -88
  59. mcp_proxy_adapter/commands/certificate_management_command.py +0 -45
  60. mcp_proxy_adapter/commands/command_registry.py +172 -904
  61. mcp_proxy_adapter/commands/config_command.py +0 -28
  62. mcp_proxy_adapter/commands/dependency_container.py +1 -70
  63. mcp_proxy_adapter/commands/dependency_manager.py +0 -128
  64. mcp_proxy_adapter/commands/echo_command.py +0 -34
  65. mcp_proxy_adapter/commands/health_command.py +0 -3
  66. mcp_proxy_adapter/commands/help_command.py +0 -159
  67. mcp_proxy_adapter/commands/hooks.py +0 -137
  68. mcp_proxy_adapter/commands/key_management_command.py +0 -25
  69. mcp_proxy_adapter/commands/load_command.py +7 -78
  70. mcp_proxy_adapter/commands/plugins_command.py +0 -16
  71. mcp_proxy_adapter/commands/protocol_management_command.py +0 -28
  72. mcp_proxy_adapter/commands/proxy_registration_command.py +0 -88
  73. mcp_proxy_adapter/commands/queue_commands.py +750 -0
  74. mcp_proxy_adapter/commands/registration_status_command.py +0 -43
  75. mcp_proxy_adapter/commands/registry/__init__.py +18 -0
  76. mcp_proxy_adapter/commands/registry/command_info.py +103 -0
  77. mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
  78. mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
  79. mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
  80. mcp_proxy_adapter/commands/reload_command.py +0 -80
  81. mcp_proxy_adapter/commands/result.py +25 -77
  82. mcp_proxy_adapter/commands/role_test_command.py +0 -44
  83. mcp_proxy_adapter/commands/roles_management_command.py +0 -199
  84. mcp_proxy_adapter/commands/security_command.py +0 -30
  85. mcp_proxy_adapter/commands/settings_command.py +0 -68
  86. mcp_proxy_adapter/commands/ssl_setup_command.py +0 -42
  87. mcp_proxy_adapter/commands/token_management_command.py +0 -1
  88. mcp_proxy_adapter/commands/transport_management_command.py +0 -20
  89. mcp_proxy_adapter/commands/unload_command.py +0 -71
  90. mcp_proxy_adapter/config.py +15 -626
  91. mcp_proxy_adapter/core/__init__.py +5 -39
  92. mcp_proxy_adapter/core/app_factory.py +14 -36
  93. mcp_proxy_adapter/core/app_runner.py +0 -27
  94. mcp_proxy_adapter/core/auth_validator.py +1 -93
  95. mcp_proxy_adapter/core/certificate/__init__.py +20 -0
  96. mcp_proxy_adapter/core/certificate/certificate_creator.py +371 -0
  97. mcp_proxy_adapter/core/certificate/certificate_extractor.py +183 -0
  98. mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
  99. mcp_proxy_adapter/core/certificate/certificate_validator.py +110 -0
  100. mcp_proxy_adapter/core/certificate/ssl_context_manager.py +70 -0
  101. mcp_proxy_adapter/core/certificate_utils.py +64 -903
  102. mcp_proxy_adapter/core/client.py +0 -6
  103. mcp_proxy_adapter/core/client_manager.py +0 -19
  104. mcp_proxy_adapter/core/client_security.py +0 -2
  105. mcp_proxy_adapter/core/config/__init__.py +18 -0
  106. mcp_proxy_adapter/core/config/config.py +195 -0
  107. mcp_proxy_adapter/core/config/config_factory.py +22 -0
  108. mcp_proxy_adapter/core/config/config_loader.py +66 -0
  109. mcp_proxy_adapter/core/config/feature_manager.py +31 -0
  110. mcp_proxy_adapter/core/config/simple_config.py +112 -0
  111. mcp_proxy_adapter/core/config/simple_config_generator.py +50 -0
  112. mcp_proxy_adapter/core/config/simple_config_validator.py +96 -0
  113. mcp_proxy_adapter/core/config_converter.py +0 -186
  114. mcp_proxy_adapter/core/config_validator.py +96 -1238
  115. mcp_proxy_adapter/core/errors.py +7 -42
  116. mcp_proxy_adapter/core/job_manager.py +54 -0
  117. mcp_proxy_adapter/core/logging.py +2 -22
  118. mcp_proxy_adapter/core/mtls_asgi.py +0 -20
  119. mcp_proxy_adapter/core/mtls_asgi_app.py +0 -12
  120. mcp_proxy_adapter/core/mtls_proxy.py +0 -80
  121. mcp_proxy_adapter/core/mtls_server.py +3 -173
  122. mcp_proxy_adapter/core/protocol_manager.py +1 -191
  123. mcp_proxy_adapter/core/proxy/__init__.py +22 -0
  124. mcp_proxy_adapter/core/proxy/auth_manager.py +27 -0
  125. mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +137 -0
  126. mcp_proxy_adapter/core/proxy/registration_client.py +60 -0
  127. mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
  128. mcp_proxy_adapter/core/proxy_client.py +0 -1
  129. mcp_proxy_adapter/core/proxy_registration.py +36 -913
  130. mcp_proxy_adapter/core/role_utils.py +0 -308
  131. mcp_proxy_adapter/core/security_adapter.py +1 -36
  132. mcp_proxy_adapter/core/security_factory.py +1 -150
  133. mcp_proxy_adapter/core/security_integration.py +0 -33
  134. mcp_proxy_adapter/core/server_adapter.py +1 -40
  135. mcp_proxy_adapter/core/server_engine.py +2 -173
  136. mcp_proxy_adapter/core/settings.py +0 -127
  137. mcp_proxy_adapter/core/signal_handler.py +0 -65
  138. mcp_proxy_adapter/core/ssl_utils.py +19 -137
  139. mcp_proxy_adapter/core/transport_manager.py +0 -151
  140. mcp_proxy_adapter/core/unified_config_adapter.py +1 -193
  141. mcp_proxy_adapter/core/utils.py +1 -182
  142. mcp_proxy_adapter/core/validation/__init__.py +21 -0
  143. mcp_proxy_adapter/core/validation/config_validator.py +211 -0
  144. mcp_proxy_adapter/core/validation/file_validator.py +73 -0
  145. mcp_proxy_adapter/core/validation/protocol_validator.py +191 -0
  146. mcp_proxy_adapter/core/validation/security_validator.py +58 -0
  147. mcp_proxy_adapter/core/validation/validation_result.py +27 -0
  148. mcp_proxy_adapter/custom_openapi.py +33 -652
  149. mcp_proxy_adapter/examples/bugfix_certificate_config.py +0 -23
  150. mcp_proxy_adapter/examples/check_config.py +0 -2
  151. mcp_proxy_adapter/examples/client_usage_example.py +164 -0
  152. mcp_proxy_adapter/examples/config_builder.py +13 -2
  153. mcp_proxy_adapter/examples/config_cli.py +0 -1
  154. mcp_proxy_adapter/examples/create_test_configs.py +0 -46
  155. mcp_proxy_adapter/examples/debug_request_state.py +0 -1
  156. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +0 -47
  157. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +0 -45
  158. mcp_proxy_adapter/examples/full_application/commands/echo_command.py +0 -12
  159. mcp_proxy_adapter/examples/full_application/commands/help_command.py +0 -12
  160. mcp_proxy_adapter/examples/full_application/commands/list_command.py +0 -7
  161. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +0 -2
  162. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +0 -59
  163. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +0 -54
  164. mcp_proxy_adapter/examples/full_application/main.py +186 -150
  165. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +0 -107
  166. mcp_proxy_adapter/examples/full_application/test_minimal_server.py +0 -24
  167. mcp_proxy_adapter/examples/full_application/test_server.py +0 -58
  168. mcp_proxy_adapter/examples/generate_config.py +65 -11
  169. mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
  170. mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
  171. mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
  172. mcp_proxy_adapter/examples/queue_server_example.py +85 -0
  173. mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
  174. mcp_proxy_adapter/examples/required_certificates.py +0 -2
  175. mcp_proxy_adapter/examples/run_full_test_suite.py +0 -29
  176. mcp_proxy_adapter/examples/run_proxy_server.py +31 -71
  177. mcp_proxy_adapter/examples/run_security_tests_fixed.py +0 -27
  178. mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
  179. mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
  180. mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
  181. mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
  182. mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
  183. mcp_proxy_adapter/examples/security_test_client.py +24 -1075
  184. mcp_proxy_adapter/examples/setup/__init__.py +24 -0
  185. mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
  186. mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
  187. mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
  188. mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
  189. mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
  190. mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
  191. mcp_proxy_adapter/examples/setup_test_environment.py +133 -1425
  192. mcp_proxy_adapter/examples/test_config.py +0 -3
  193. mcp_proxy_adapter/examples/test_config_builder.py +25 -405
  194. mcp_proxy_adapter/examples/test_examples.py +0 -1
  195. mcp_proxy_adapter/examples/test_framework_complete.py +0 -2
  196. mcp_proxy_adapter/examples/test_mcp_server.py +0 -1
  197. mcp_proxy_adapter/examples/test_protocol_examples.py +0 -1
  198. mcp_proxy_adapter/examples/universal_client.py +0 -6
  199. mcp_proxy_adapter/examples/update_config_certificates.py +0 -1
  200. mcp_proxy_adapter/examples/validate_generator_compatibility.py +0 -1
  201. mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +0 -187
  202. mcp_proxy_adapter/integrations/__init__.py +25 -0
  203. mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
  204. mcp_proxy_adapter/main.py +70 -62
  205. mcp_proxy_adapter/openapi.py +0 -22
  206. mcp_proxy_adapter/version.py +1 -1
  207. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/METADATA +2 -1
  208. mcp_proxy_adapter-6.9.29.dist-info/RECORD +235 -0
  209. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/entry_points.txt +1 -1
  210. mcp_proxy_adapter-6.9.28.dist-info/RECORD +0 -149
  211. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/WHEEL +0 -0
  212. {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.29.dist-info}/top_level.txt +0 -0
@@ -6,962 +6,92 @@ This module handles the command catalog, including:
6
6
  - Version comparison and updates
7
7
  - Local command storage and management
8
8
  - Automatic dependency installation
9
+
10
+ Author: Vasiliy Zdanovskiy
11
+ email: vasilyvz@gmail.com
9
12
  """
10
13
 
11
- import json
12
- import os
13
- import shutil
14
- import tempfile
15
- from pathlib import Path
16
14
  from typing import Dict, List, Optional, Any
17
- from packaging import version as pkg_version
18
15
 
19
- from mcp_proxy_adapter.core.logging import get_global_logger
20
- from mcp_proxy_adapter.commands.dependency_manager import dependency_manager
21
- from mcp_proxy_adapter.config import config
16
+ from .catalog import CommandCatalog, CatalogManager
22
17
 
23
- # Try to import requests, but don't fail if not available
24
- try:
25
- import requests
26
18
 
27
- REQUESTS_AVAILABLE = True
28
- except ImportError:
29
- REQUESTS_AVAILABLE = False
30
- get_global_logger().warning(
31
- "requests library not available, HTTP/HTTPS functionality will be limited"
32
- )
19
+ # Global catalog manager instance
20
+ _catalog_manager: Optional[CatalogManager] = None
33
21
 
34
22
 
35
- class CommandCatalog:
23
+ def get_catalog_manager() -> CatalogManager:
36
24
  """
37
- Represents a command in the catalog with metadata.
25
+ Get the global catalog manager instance.
26
+
27
+ Returns:
28
+ Catalog manager instance
38
29
  """
30
+ global _catalog_manager
31
+ if _catalog_manager is None:
32
+ from mcp_proxy_adapter.config import config
33
+ catalog_dir = config.get("catalog", {}).get("directory", "catalog")
34
+ _catalog_manager = CatalogManager(catalog_dir)
35
+ return _catalog_manager
39
36
 
40
- def __init__(
41
- self, name: str, version: str, source_url: str, file_path: Optional[str] = None
42
- ):
43
- self.name = name
44
- self.version = version
45
- self.source_url = source_url
46
- self.file_path = file_path
47
- self.metadata: Dict[str, Any] = {}
48
37
 
49
- # Standard metadata fields
50
- self.plugin: Optional[str] = None
51
- self.descr: Optional[str] = None
52
- self.category: Optional[str] = None
53
- self.author: Optional[str] = None
54
- self.email: Optional[str] = None
55
- self.depends: Optional[List[str]] = None # New field for dependencies
38
+ def get_catalog_from_server(server_url: str) -> Dict[str, CommandCatalog]:
39
+ """
40
+ Get catalog from remote server.
56
41
 
57
- def to_dict(self) -> Dict[str, Any]:
58
- """Convert to dictionary for serialization."""
59
- return {
60
- "name": self.name,
61
- "version": self.version,
62
- "source_url": self.source_url,
63
- "file_path": self.file_path,
64
- "plugin": self.plugin,
65
- "descr": self.descr,
66
- "category": self.category,
67
- "author": self.author,
68
- "email": self.email,
69
- "depends": self.depends,
70
- "metadata": self.metadata,
71
- }
42
+ Args:
43
+ server_url: Server URL to load catalog from
72
44
 
73
- @classmethod
74
- def from_dict(cls, data: Dict[str, Any]) -> "CommandCatalog":
75
- """Create from dictionary."""
76
- catalog = cls(
77
- name=data["name"],
78
- version=data["version"],
79
- source_url=data["source_url"],
80
- file_path=data.get("file_path"),
81
- )
45
+ Returns:
46
+ Dictionary of command catalogs
47
+ """
48
+ return get_catalog_manager().get_catalog_from_server(server_url)
82
49
 
83
- # Load standard metadata fields
84
- catalog.plugin = data.get("plugin")
85
- catalog.descr = data.get("descr")
86
- catalog.category = data.get("category")
87
- catalog.author = data.get("author")
88
- catalog.email = data.get("email")
89
50
 
90
- # Handle depends field
91
- depends = data.get("depends")
92
- if depends and isinstance(depends, list):
93
- catalog.depends = depends
94
- elif depends and isinstance(depends, str):
95
- catalog.depends = [depends]
96
- else:
97
- catalog.depends = depends
51
+ def sync_with_servers(server_urls: List[str]) -> Dict[str, Any]:
52
+ """
53
+ Synchronize catalog with remote servers.
98
54
 
99
- catalog.metadata = data.get("metadata", {})
55
+ Args:
56
+ server_urls: List of server URLs to sync with
100
57
 
101
- return catalog
58
+ Returns:
59
+ Dictionary with sync results
60
+ """
61
+ return get_catalog_manager().sync_with_servers(server_urls)
102
62
 
103
63
 
104
- class CatalogManager:
64
+ def get_local_commands() -> List[str]:
105
65
  """
106
- Manages the command catalog system.
66
+ Get list of locally available commands.
107
67
 
108
- The catalog is loaded fresh from servers each time - no caching.
109
- It only contains the list of available plugins.
68
+ Returns:
69
+ List of command names
110
70
  """
71
+ return get_catalog_manager().get_local_commands()
111
72
 
112
- def __init__(self, catalog_dir: str):
113
- """
114
- Initialize catalog manager.
115
-
116
- Args:
117
- catalog_dir: Directory to store downloaded command files
118
- """
119
- self.catalog_dir = Path(catalog_dir)
120
- self.commands_dir = self.catalog_dir / "commands"
121
-
122
- # Ensure directories exist
123
- self.catalog_dir.mkdir(parents=True, exist_ok=True)
124
- self.commands_dir.mkdir(parents=True, exist_ok=True)
125
-
126
- # No local catalog caching - always fetch fresh from servers
127
- self.catalog: Dict[str, CommandCatalog] = {}
128
-
129
- def _load_catalog(self) -> None:
130
- """DEPRECATED: Catalog is not cached locally."""
131
- get_global_logger().warning(
132
- "_load_catalog() is deprecated - catalog is always fetched fresh from servers"
133
- )
134
-
135
- def _save_catalog(self) -> None:
136
- """DEPRECATED: Catalog is not cached locally."""
137
- get_global_logger().warning(
138
- "_save_catalog() is deprecated - catalog is always fetched fresh from servers"
139
- )
140
-
141
- def _parse_catalog_data(
142
- self, server_catalog: Dict[str, Any], server_url: str = "http://example.com"
143
- ) -> Dict[str, CommandCatalog]:
144
- """
145
- Parse catalog data from server response.
146
-
147
- Args:
148
- server_catalog: Raw catalog data from server
149
- server_url: URL of the server (for constructing source URLs)
150
-
151
- Returns:
152
- Dictionary of parsed CommandCatalog objects
153
- """
154
- if not isinstance(server_catalog, dict):
155
- get_global_logger().error(
156
- f"Invalid catalog format: expected dict, got {type(server_catalog)}"
157
- )
158
- return {}
159
-
160
- result = {}
161
-
162
- # Support both old format (commands array) and new format (id-value pairs)
163
- if "commands" in server_catalog:
164
- # Old format: {"commands": [{"name": "...", ...}]}
165
- commands_data = server_catalog.get("commands", [])
166
-
167
- if not isinstance(commands_data, list):
168
- get_global_logger().error(
169
- f"Invalid commands format: expected list, got {type(commands_data)}"
170
- )
171
- return {}
172
-
173
- for cmd_data in commands_data:
174
- try:
175
- if not isinstance(cmd_data, dict):
176
- get_global_logger().warning(
177
- f"Skipping invalid command data: expected dict, got {type(cmd_data)}"
178
- )
179
- continue
180
-
181
- name = cmd_data.get("name")
182
- if not name or not isinstance(name, str):
183
- get_global_logger().warning(f"Skipping command without valid name")
184
- continue
185
-
186
- version = cmd_data.get("version", "0.1")
187
- if not isinstance(version, str):
188
- get_global_logger().warning(f"Invalid version format for {name}: {version}")
189
- version = "0.1"
190
-
191
- source_url = cmd_data.get("source_url", "")
192
- if not isinstance(source_url, str):
193
- get_global_logger().warning(f"Invalid source_url for {name}: {source_url}")
194
- source_url = ""
195
-
196
- catalog = CommandCatalog(
197
- name=name, version=version, source_url=source_url
198
- )
199
-
200
- # Load standard metadata fields
201
- catalog.plugin = cmd_data.get("plugin")
202
- catalog.descr = cmd_data.get("descr")
203
- catalog.category = cmd_data.get("category")
204
- catalog.author = cmd_data.get("author")
205
- catalog.email = cmd_data.get("email")
206
- catalog.metadata = cmd_data
207
-
208
- result[name] = catalog
209
-
210
- except Exception as e:
211
- get_global_logger().error(f"Error processing command data: {e}")
212
- continue
213
- else:
214
- # New format: {"id": {"plugin": "...", "descr": "...", ...}}
215
- for command_id, cmd_data in server_catalog.items():
216
- try:
217
- if not isinstance(cmd_data, dict):
218
- get_global_logger().warning(
219
- f"Skipping invalid command data for {command_id}: expected dict, got {type(cmd_data)}"
220
- )
221
- continue
222
-
223
- # Use command_id as name if name is not provided
224
- name = cmd_data.get("name", command_id)
225
- if not isinstance(name, str):
226
- get_global_logger().warning(
227
- f"Skipping command {command_id} without valid name"
228
- )
229
- continue
230
-
231
- version = cmd_data.get("version", "0.1")
232
- if not isinstance(version, str):
233
- get_global_logger().warning(f"Invalid version format for {name}: {version}")
234
- version = "0.1"
235
-
236
- # For new format, construct source_url from server_url and plugin filename
237
- plugin_file = cmd_data.get("plugin")
238
- if plugin_file and isinstance(plugin_file, str):
239
- # Construct source URL by appending plugin filename to server URL
240
- if server_url.endswith("/"):
241
- source_url = f"{server_url}{plugin_file}"
242
- else:
243
- source_url = f"{server_url}/{plugin_file}"
244
- else:
245
- source_url = server_url
246
-
247
- catalog = CommandCatalog(
248
- name=name, version=version, source_url=source_url
249
- )
250
-
251
- # Load standard metadata fields
252
- catalog.plugin = cmd_data.get("plugin")
253
- catalog.descr = cmd_data.get("descr")
254
- catalog.category = cmd_data.get("category")
255
- catalog.author = cmd_data.get("author")
256
- catalog.email = cmd_data.get("email")
257
-
258
- # Load dependencies field
259
- depends = cmd_data.get("depends")
260
- if depends and isinstance(depends, list):
261
- catalog.depends = depends
262
- elif depends and isinstance(depends, str):
263
- # Handle case where depends is a single string
264
- catalog.depends = [depends]
265
-
266
- # Store full metadata including new fields like "depends"
267
- catalog.metadata = cmd_data
268
-
269
- result[name] = catalog
270
-
271
- except Exception as e:
272
- get_global_logger().error(f"Error processing command {command_id}: {e}")
273
- continue
274
-
275
- return result
276
-
277
- def get_catalog_from_server(self, server_url: str) -> Dict[str, CommandCatalog]:
278
- """
279
- Fetch command catalog from remote server.
280
-
281
- Args:
282
- server_url: URL of the plugin server
283
-
284
- Returns:
285
- Dictionary of commands available on the server
286
- """
287
- if not REQUESTS_AVAILABLE:
288
- get_global_logger().error(
289
- "requests library not available, cannot fetch catalog from remote server"
290
- )
291
- return {}
292
-
293
- try:
294
- # Validate URL format
295
- if not server_url.startswith(("http://", "https://")):
296
- get_global_logger().error(f"Invalid server URL format: {server_url}")
297
- return {}
298
-
299
- # Fetch catalog from server (use URL as-is)
300
- get_global_logger().debug(f"Fetching catalog from: {server_url}")
301
-
302
- response = requests.get(server_url, timeout=30)
303
- response.raise_for_status()
304
-
305
- # Validate response content
306
- if not response.content:
307
- get_global_logger().error(f"Empty response from {server_url}")
308
- return {}
309
-
310
- try:
311
- server_catalog = response.json()
312
- except json.JSONDecodeError as e:
313
- get_global_logger().error(f"Invalid JSON response from {server_url}: {e}")
314
- return {}
315
-
316
- if not isinstance(server_catalog, dict):
317
- get_global_logger().error(
318
- f"Invalid catalog format from {server_url}: expected dict, got {type(server_catalog)}"
319
- )
320
- return {}
321
-
322
- result = self._parse_catalog_data(server_catalog, server_url)
323
-
324
- get_global_logger().info(
325
- f"Successfully fetched catalog from {server_url}: {len(result)} commands"
326
- )
327
- return result
328
-
329
- except requests.exceptions.Timeout:
330
- get_global_logger().error(f"Timeout while fetching catalog from {server_url}")
331
- return {}
332
- except requests.exceptions.ConnectionError as e:
333
- get_global_logger().error(
334
- f"Connection error while fetching catalog from {server_url}: {e}"
335
- )
336
- return {}
337
- except requests.exceptions.HTTPError as e:
338
- get_global_logger().error(f"HTTP error while fetching catalog from {server_url}: {e}")
339
- return {}
340
- except requests.exceptions.RequestException as e:
341
- get_global_logger().error(f"Request error while fetching catalog from {server_url}: {e}")
342
- return {}
343
- except Exception as e:
344
- get_global_logger().error(
345
- f"Unexpected error while fetching catalog from {server_url}: {e}"
346
- )
347
- return {}
348
-
349
- def _check_dependencies(
350
- self, command_name: str, server_cmd: CommandCatalog
351
- ) -> bool:
352
- """
353
- Check if command dependencies are satisfied.
354
-
355
- Args:
356
- command_name: Name of the command
357
- server_cmd: Command catalog entry from server
358
-
359
- Returns:
360
- True if dependencies are satisfied, False otherwise
361
- """
362
- if not server_cmd.depends:
363
- return True
364
-
365
- all_satisfied, missing_deps, installed_deps = (
366
- dependency_manager.check_dependencies(server_cmd.depends)
367
- )
368
-
369
- if not all_satisfied:
370
- get_global_logger().warning(
371
- f"Command {command_name} has missing dependencies: {missing_deps}"
372
- )
373
- get_global_logger().info(f"Installed dependencies: {installed_deps}")
374
- return False
375
-
376
- get_global_logger().debug(
377
- f"All dependencies satisfied for command {command_name}: {server_cmd.depends}"
378
- )
379
- return True
380
-
381
- def _install_dependencies(
382
- self, command_name: str, server_cmd: CommandCatalog, auto_install: bool = None
383
- ) -> bool:
384
- """
385
- Install command dependencies automatically.
386
-
387
- Args:
388
- command_name: Name of the command
389
- server_cmd: Command catalog entry from server
390
- auto_install: Whether to automatically install missing dependencies (None = use config)
391
-
392
- Returns:
393
- True if all dependencies are satisfied, False otherwise
394
- """
395
- if not server_cmd.depends:
396
- return True
397
-
398
- # Use config setting if auto_install is None
399
- if auto_install is None:
400
- auto_install = config.get("commands.auto_install_dependencies", True)
401
-
402
- # Check current dependencies
403
- all_satisfied, missing_deps, installed_deps = (
404
- dependency_manager.check_dependencies(server_cmd.depends)
405
- )
406
-
407
- if all_satisfied:
408
- get_global_logger().info(
409
- f"All dependencies already satisfied for {command_name}: {server_cmd.depends}"
410
- )
411
- return True
412
-
413
- if not auto_install:
414
- get_global_logger().warning(
415
- f"Command {command_name} has missing dependencies: {missing_deps}"
416
- )
417
- get_global_logger().info(
418
- f"Auto-install is disabled. Please install manually: pip install {' '.join(missing_deps)}"
419
- )
420
- return False
421
-
422
- # Try to install missing dependencies
423
- get_global_logger().info(
424
- f"Installing missing dependencies for {command_name}: {missing_deps}"
425
- )
426
-
427
- success, installed_deps, failed_deps = dependency_manager.install_dependencies(
428
- missing_deps
429
- )
430
-
431
- if success:
432
- get_global_logger().info(
433
- f"Successfully installed all dependencies for {command_name}: {installed_deps}"
434
- )
435
-
436
- # Verify installation
437
- all_verified, failed_verifications = dependency_manager.verify_installation(
438
- server_cmd.depends
439
- )
440
- if all_verified:
441
- get_global_logger().info(f"All dependencies verified for {command_name}")
442
- return True
443
- else:
444
- get_global_logger().error(
445
- f"Failed to verify dependencies for {command_name}: {failed_verifications}"
446
- )
447
- return False
448
- else:
449
- get_global_logger().error(
450
- f"Failed to install dependencies for {command_name}: {failed_deps}"
451
- )
452
- get_global_logger().error(
453
- f"Please install manually: pip install {' '.join(failed_deps)}"
454
- )
455
- return False
456
-
457
- def _should_download_command(
458
- self, command_name: str, server_cmd: CommandCatalog
459
- ) -> bool:
460
- """
461
- Check if command should be downloaded based on version comparison.
462
-
463
- Args:
464
- command_name: Name of the command
465
- server_cmd: Command catalog entry from server
466
-
467
- Returns:
468
- True if command should be downloaded, False otherwise
469
- """
470
- local_file = self.commands_dir / f"{command_name}_command.py"
471
-
472
- # If local file doesn't exist, download
473
- if not local_file.exists():
474
- get_global_logger().info(f"New command found: {command_name} v{server_cmd.version}")
475
- return True
476
-
477
- # Try to extract version from local file
478
- try:
479
- local_metadata = self.extract_metadata_from_file(str(local_file))
480
- local_version = local_metadata.get("version", "0.0")
481
-
482
- # Compare versions
483
- server_ver = pkg_version.parse(server_cmd.version)
484
- local_ver = pkg_version.parse(local_version)
485
-
486
- if server_ver > local_ver:
487
- get_global_logger().info(
488
- f"Newer version available for {command_name}: {local_ver} -> {server_ver}"
489
- )
490
- return True
491
- else:
492
- get_global_logger().debug(
493
- f"Local version {local_ver} is same or newer than server version {server_ver}"
494
- )
495
- return False
496
-
497
- except Exception as e:
498
- get_global_logger().warning(f"Failed to compare versions for {command_name}: {e}")
499
- # If version comparison fails, download anyway
500
- return True
501
-
502
- def update_command(
503
- self, command_name: str, server_catalog: Dict[str, CommandCatalog]
504
- ) -> bool:
505
- """
506
- DEPRECATED: Always download fresh from server.
507
-
508
- Args:
509
- command_name: Name of the command to update
510
- server_catalog: Catalog from server
511
-
512
- Returns:
513
- True if command was downloaded, False otherwise
514
- """
515
- get_global_logger().warning(
516
- "update_command() is deprecated - always downloading fresh from server (use _download_command directly)"
517
- )
518
-
519
- if command_name not in server_catalog:
520
- return False
521
-
522
- server_cmd = server_catalog[command_name]
523
- return self._download_command(command_name, server_cmd)
524
73
 
525
- def _download_command(self, command_name: str, server_cmd: CommandCatalog) -> bool:
526
- """
527
- Download command file from server.
528
-
529
- Args:
530
- command_name: Name of the command
531
- server_cmd: Command catalog entry from server
532
-
533
- Returns:
534
- True if download successful, False otherwise
535
- """
536
- if not REQUESTS_AVAILABLE:
537
- get_global_logger().error(
538
- "requests library not available, cannot download command from remote server"
539
- )
540
- return False
541
-
542
- # Step 1: Check and install dependencies
543
- if not self._install_dependencies(command_name, server_cmd):
544
- get_global_logger().error(
545
- f"Cannot download {command_name}: failed to install dependencies"
546
- )
547
- return False
548
-
549
- try:
550
- # Validate source URL
551
- if not server_cmd.source_url.startswith(("http://", "https://")):
552
- get_global_logger().error(
553
- f"Invalid source URL for {command_name}: {server_cmd.source_url}"
554
- )
555
- return False
556
-
557
- # Download command file (use source_url as-is)
558
- get_global_logger().debug(f"Downloading {command_name} from: {server_cmd.source_url}")
559
-
560
- response = requests.get(server_cmd.source_url, timeout=30)
561
- response.raise_for_status()
562
-
563
- # Validate response content
564
- if not response.content:
565
- get_global_logger().error(
566
- f"Empty response when downloading {command_name} from {server_cmd.source_url}"
567
- )
568
- return False
569
-
570
- # Validate Python file content
571
- content = response.text
572
- if not content.strip():
573
- get_global_logger().error(
574
- f"Empty file content for {command_name} from {server_cmd.source_url}"
575
- )
576
- return False
577
-
578
- # Basic Python file validation - only warn for clearly invalid files
579
- content_stripped = content.strip()
580
- if (
581
- content_stripped
582
- and not content_stripped.startswith(
583
- ('"""', "'''", "#", "from ", "import ", "class ", "def ")
584
- )
585
- and not any(
586
- keyword in content_stripped
587
- for keyword in ["class", "def", "import", "from", '"""', "'''", "#"]
588
- )
589
- ):
590
- get_global_logger().warning(
591
- f"File {command_name}_command.py doesn't look like a valid Python file"
592
- )
593
-
594
- # Create temporary file first for validation
595
- temp_file = None
596
- try:
597
- temp_file = tempfile.NamedTemporaryFile(
598
- mode="w", suffix=".py", delete=False
599
- )
600
- temp_file.write(content)
601
- temp_file.close()
602
-
603
- # Try to import the module to validate it
604
- try:
605
- import importlib.util
606
-
607
- spec = importlib.util.spec_from_file_location(
608
- f"{command_name}_command", temp_file.name
609
- )
610
- if spec and spec.loader:
611
- module = importlib.util.module_from_spec(spec)
612
- spec.loader.exec_module(module)
613
-
614
- # Check if module contains a Command class
615
- if not hasattr(module, "Command") and not any(
616
- hasattr(module, attr) and attr.endswith("Command")
617
- for attr in dir(module)
618
- ):
619
- get_global_logger().warning(
620
- f"Module {command_name}_command.py doesn't contain a Command class"
621
- )
622
-
623
- except Exception as e:
624
- get_global_logger().error(f"Failed to validate {command_name}_command.py: {e}")
625
- return False
626
-
627
- # If validation passed, move to final location
628
- file_path = self.commands_dir / f"{command_name}_command.py"
629
-
630
- # Remove existing file if it exists
631
- if file_path.exists():
632
- file_path.unlink()
633
-
634
- # Move temporary file to final location
635
- shutil.move(temp_file.name, file_path)
636
-
637
- # Update catalog entry
638
- server_cmd.file_path = str(file_path)
639
- self.catalog[command_name] = server_cmd
640
-
641
- # Extract metadata from downloaded file
642
- try:
643
- metadata = self.extract_metadata_from_file(str(file_path))
644
- if metadata:
645
- # Update standard fields
646
- if "plugin" in metadata:
647
- server_cmd.plugin = metadata["plugin"]
648
- if "descr" in metadata:
649
- server_cmd.descr = metadata["descr"]
650
- if "category" in metadata:
651
- server_cmd.category = metadata["category"]
652
- if "author" in metadata:
653
- server_cmd.author = metadata["author"]
654
- if "email" in metadata:
655
- server_cmd.email = metadata["email"]
656
- if "version" in metadata:
657
- server_cmd.version = metadata["version"]
658
-
659
- # Update full metadata
660
- server_cmd.metadata.update(metadata)
661
-
662
- get_global_logger().debug(
663
- f"Extracted metadata for {command_name}: {metadata}"
664
- )
665
- except Exception as e:
666
- get_global_logger().warning(
667
- f"Failed to extract metadata from {command_name}: {e}"
668
- )
669
-
670
- get_global_logger().info(
671
- f"Successfully downloaded and validated {command_name} v{server_cmd.version}"
672
- )
673
- return True
674
-
675
- finally:
676
- # Clean up temporary file if it still exists
677
- if temp_file and os.path.exists(temp_file.name):
678
- try:
679
- os.unlink(temp_file.name)
680
- except Exception as e:
681
- get_global_logger().warning(
682
- f"Failed to clean up temporary file {temp_file.name}: {e}"
683
- )
684
-
685
- except requests.exceptions.Timeout:
686
- get_global_logger().error(
687
- f"Timeout while downloading {command_name} from {server_cmd.source_url}"
688
- )
689
- return False
690
- except requests.exceptions.ConnectionError as e:
691
- get_global_logger().error(
692
- f"Connection error while downloading {command_name} from {server_cmd.source_url}: {e}"
693
- )
694
- return False
695
- except requests.exceptions.HTTPError as e:
696
- get_global_logger().error(
697
- f"HTTP error while downloading {command_name} from {server_cmd.source_url}: {e}"
698
- )
699
- return False
700
- except requests.exceptions.RequestException as e:
701
- get_global_logger().error(
702
- f"Request error while downloading {command_name} from {server_cmd.source_url}: {e}"
703
- )
704
- return False
705
- except OSError as e:
706
- get_global_logger().error(f"File system error while downloading {command_name}: {e}")
707
- return False
708
- except Exception as e:
709
- get_global_logger().error(f"Unexpected error while downloading {command_name}: {e}")
710
- return False
711
-
712
- def sync_with_servers(self, server_urls: List[str]) -> Dict[str, Any]:
713
- """
714
- Sync with remote servers - load fresh catalog each time.
715
-
716
- Args:
717
- server_urls: List of server URLs to sync with
718
-
719
- Returns:
720
- Dictionary with sync results
721
- """
722
- get_global_logger().info(f"Loading fresh catalog from {len(server_urls)} servers")
723
-
724
- # Clear local catalog - always start fresh
725
- self.catalog = {}
726
-
727
- results = {
728
- "servers_processed": 0,
729
- "commands_updated": 0,
730
- "commands_added": 0,
731
- "errors": [],
732
- }
733
-
734
- for server_url in server_urls:
735
- try:
736
- # Get fresh catalog from server
737
- server_catalog = self.get_catalog_from_server(server_url)
738
- if not server_catalog:
739
- continue
740
-
741
- results["servers_processed"] += 1
742
-
743
- # Process each command from server catalog
744
- for command_name, server_cmd in server_catalog.items():
745
- # Check if we need to download/update
746
- if self._should_download_command(command_name, server_cmd):
747
- if self._download_command(command_name, server_cmd):
748
- results["commands_added"] += 1
749
- # Add to local catalog for this session
750
- self.catalog[command_name] = server_cmd
751
- else:
752
- # Command already exists with same or newer version
753
- get_global_logger().debug(
754
- f"Command {command_name} already exists with same or newer version"
755
- )
756
-
757
- except Exception as e:
758
- error_msg = f"Failed to sync with {server_url}: {e}"
759
- results["errors"].append(error_msg)
760
- get_global_logger().error(error_msg)
761
-
762
- get_global_logger().info(f"Fresh catalog loaded: {results}")
763
- return results
764
-
765
- def get_local_commands(self) -> List[str]:
766
- """
767
- Get list of locally available command files.
768
-
769
- Returns:
770
- List of command file paths
771
- """
772
- commands = []
773
- for file_path in self.commands_dir.glob("*_command.py"):
774
- commands.append(str(file_path))
775
-
776
- get_global_logger().debug(
777
- f"Found {len(commands)} local command files: {[Path(c).name for c in commands]}"
778
- )
779
- return commands
780
-
781
- def get_command_info(self, command_name: str) -> Optional[CommandCatalog]:
782
- """
783
- Get information about a command in the catalog.
784
-
785
- Args:
786
- command_name: Name of the command
787
-
788
- Returns:
789
- Command catalog entry or None if not found
790
- """
791
- return self.catalog.get(command_name)
792
-
793
- def remove_command(self, command_name: str) -> bool:
794
- """
795
- Remove command from catalog and delete file.
796
-
797
- Args:
798
- command_name: Name of the command to remove
799
-
800
- Returns:
801
- True if removed successfully, False otherwise
802
- """
803
- if command_name not in self.catalog:
804
- return False
805
-
806
- try:
807
- # Remove file
808
- cmd = self.catalog[command_name]
809
- if cmd.file_path and os.path.exists(cmd.file_path):
810
- os.remove(cmd.file_path)
811
-
812
- # Remove from catalog
813
- del self.catalog[command_name]
814
- self._save_catalog()
815
-
816
- get_global_logger().info(f"Removed command: {command_name}")
817
- return True
818
-
819
- except Exception as e:
820
- get_global_logger().error(f"Failed to remove command {command_name}: {e}")
821
- return False
822
-
823
- def extract_metadata_from_file(self, file_path: str) -> Dict[str, Any]:
824
- """
825
- Extract metadata from command file.
826
-
827
- Args:
828
- file_path: Path to command file
829
-
830
- Returns:
831
- Dictionary with extracted metadata
832
- """
833
- metadata = {}
834
-
835
- try:
836
- with open(file_path, "r", encoding="utf-8") as f:
837
- content = f.read()
838
-
839
- # Look for metadata in docstring or comments
840
- lines = content.split("\n")
841
-
842
- for line in lines:
843
- line = line.strip()
844
-
845
- # Look for metadata patterns
846
- if (
847
- line.startswith("#")
848
- or line.startswith('"""')
849
- or line.startswith("'''")
850
- ):
851
- # Try to parse JSON-like metadata
852
- if "{" in line and "}" in line:
853
- try:
854
- # Extract JSON part
855
- start = line.find("{")
856
- end = line.rfind("}") + 1
857
- json_str = line[start:end]
858
-
859
- # Parse JSON
860
- import json
861
-
862
- parsed = json.loads(json_str)
863
-
864
- # Update metadata
865
- metadata.update(parsed)
866
-
867
- except json.JSONDecodeError:
868
- # Not valid JSON, continue
869
- continue
870
-
871
- # Look for specific metadata patterns
872
- for pattern in [
873
- "plugin:",
874
- "descr:",
875
- "category:",
876
- "author:",
877
- "version:",
878
- "email:",
879
- ]:
880
- if pattern in line:
881
- key = pattern.rstrip(":")
882
- value = line.split(pattern, 1)[1].strip().strip("\"'")
883
- metadata[key] = value
884
-
885
- # Also check for JSON in docstring blocks
886
- content_str = content
887
- if '"""' in content_str or "'''" in content_str:
888
- # Find docstring blocks
889
- import re
890
-
891
- docstring_pattern = r'"""(.*?)"""|\'\'\'(.*?)\'\'\''
892
- matches = re.findall(docstring_pattern, content_str, re.DOTALL)
893
-
894
- for match in matches:
895
- docstring = match[0] if match[0] else match[1]
896
- # Look for JSON in docstring
897
- if "{" in docstring and "}" in docstring:
898
- try:
899
- # Find JSON object
900
- start = docstring.find("{")
901
- end = docstring.rfind("}") + 1
902
- json_str = docstring[start:end]
903
-
904
- import json
905
-
906
- parsed = json.loads(json_str)
907
- metadata.update(parsed)
908
-
909
- except json.JSONDecodeError:
910
- continue
911
-
912
- get_global_logger().debug(f"Extracted metadata from {file_path}: {metadata}")
913
- return metadata
914
-
915
- except Exception as e:
916
- get_global_logger().error(f"Failed to extract metadata from {file_path}: {e}")
917
- return {}
918
-
919
- def update_local_command_metadata(self, command_name: str) -> bool:
920
- """
921
- Update metadata for a local command by reading its file.
922
-
923
- Args:
924
- command_name: Name of the command
925
-
926
- Returns:
927
- True if updated successfully, False otherwise
928
- """
929
- if command_name not in self.catalog:
930
- return False
931
-
932
- cmd = self.catalog[command_name]
933
- if not cmd.file_path or not os.path.exists(cmd.file_path):
934
- return False
935
-
936
- try:
937
- metadata = self.extract_metadata_from_file(cmd.file_path)
74
+ def get_command_info(command_name: str) -> Optional[CommandCatalog]:
75
+ """
76
+ Get information about a specific command.
938
77
 
939
- if metadata:
940
- # Update standard fields
941
- if "plugin" in metadata:
942
- cmd.plugin = metadata["plugin"]
943
- if "descr" in metadata:
944
- cmd.descr = metadata["descr"]
945
- if "category" in metadata:
946
- cmd.category = metadata["category"]
947
- if "author" in metadata:
948
- cmd.author = metadata["author"]
949
- if "email" in metadata:
950
- cmd.email = metadata["email"]
951
- if "version" in metadata:
952
- cmd.version = metadata["version"]
78
+ Args:
79
+ command_name: Name of the command
953
80
 
954
- # Update full metadata
955
- cmd.metadata.update(metadata)
81
+ Returns:
82
+ Command catalog entry or None if not found
83
+ """
84
+ return get_catalog_manager().get_command_info(command_name)
956
85
 
957
- # Save catalog
958
- self._save_catalog()
959
86
 
960
- get_global_logger().info(f"Updated metadata for {command_name}")
961
- return True
87
+ def remove_command(command_name: str) -> bool:
88
+ """
89
+ Remove command from catalog.
962
90
 
963
- return False
91
+ Args:
92
+ command_name: Name of the command to remove
964
93
 
965
- except Exception as e:
966
- get_global_logger().error(f"Failed to update metadata for {command_name}: {e}")
967
- return False
94
+ Returns:
95
+ True if successful, False otherwise
96
+ """
97
+ return get_catalog_manager().remove_command(command_name)