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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. mcp_proxy_adapter/__main__.py +32 -0
  2. mcp_proxy_adapter/api/app.py +290 -33
  3. mcp_proxy_adapter/api/handlers.py +32 -6
  4. mcp_proxy_adapter/api/middleware/__init__.py +38 -32
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +243 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/protocol_middleware.py +201 -0
  10. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  11. mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
  12. mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
  13. mcp_proxy_adapter/commands/__init__.py +19 -4
  14. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  15. mcp_proxy_adapter/commands/base.py +66 -32
  16. mcp_proxy_adapter/commands/builtin_commands.py +95 -0
  17. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  18. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  19. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  20. mcp_proxy_adapter/commands/command_registry.py +711 -354
  21. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  22. mcp_proxy_adapter/commands/echo_command.py +81 -0
  23. mcp_proxy_adapter/commands/health_command.py +8 -1
  24. mcp_proxy_adapter/commands/help_command.py +21 -14
  25. mcp_proxy_adapter/commands/hooks.py +200 -167
  26. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  27. mcp_proxy_adapter/commands/load_command.py +176 -0
  28. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  29. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  30. mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
  31. mcp_proxy_adapter/commands/reload_command.py +48 -50
  32. mcp_proxy_adapter/commands/result.py +1 -0
  33. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  34. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  35. mcp_proxy_adapter/commands/security_command.py +488 -0
  36. mcp_proxy_adapter/commands/ssl_setup_command.py +366 -0
  37. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  38. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  39. mcp_proxy_adapter/commands/unload_command.py +158 -0
  40. mcp_proxy_adapter/config.py +394 -14
  41. mcp_proxy_adapter/core/app_factory.py +410 -0
  42. mcp_proxy_adapter/core/app_runner.py +272 -0
  43. mcp_proxy_adapter/core/auth_validator.py +606 -0
  44. mcp_proxy_adapter/core/certificate_utils.py +1045 -0
  45. mcp_proxy_adapter/core/client.py +574 -0
  46. mcp_proxy_adapter/core/client_manager.py +284 -0
  47. mcp_proxy_adapter/core/client_security.py +384 -0
  48. mcp_proxy_adapter/core/config_converter.py +405 -0
  49. mcp_proxy_adapter/core/config_validator.py +218 -0
  50. mcp_proxy_adapter/core/logging.py +19 -3
  51. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  52. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  53. mcp_proxy_adapter/core/protocol_manager.py +385 -0
  54. mcp_proxy_adapter/core/proxy_client.py +602 -0
  55. mcp_proxy_adapter/core/proxy_registration.py +522 -0
  56. mcp_proxy_adapter/core/role_utils.py +426 -0
  57. mcp_proxy_adapter/core/security_adapter.py +370 -0
  58. mcp_proxy_adapter/core/security_factory.py +239 -0
  59. mcp_proxy_adapter/core/security_integration.py +286 -0
  60. mcp_proxy_adapter/core/server_adapter.py +282 -0
  61. mcp_proxy_adapter/core/server_engine.py +270 -0
  62. mcp_proxy_adapter/core/settings.py +1 -0
  63. mcp_proxy_adapter/core/ssl_utils.py +234 -0
  64. mcp_proxy_adapter/core/transport_manager.py +292 -0
  65. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  66. mcp_proxy_adapter/custom_openapi.py +22 -11
  67. mcp_proxy_adapter/examples/__init__.py +13 -4
  68. mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
  69. mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
  70. mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
  71. mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
  72. mcp_proxy_adapter/examples/commands/__init__.py +5 -0
  73. mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
  74. mcp_proxy_adapter/examples/debug_request_state.py +112 -0
  75. mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
  76. mcp_proxy_adapter/examples/demo_client.py +275 -0
  77. mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
  78. mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
  79. mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
  80. mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
  81. mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
  82. mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
  83. mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
  84. mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  85. mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
  86. mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
  87. mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  88. mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
  89. mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
  90. mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
  91. mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
  92. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
  93. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  94. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
  95. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
  96. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  97. mcp_proxy_adapter/examples/full_application/main.py +173 -0
  98. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
  99. mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
  100. mcp_proxy_adapter/examples/generate_certificates.py +177 -0
  101. mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
  102. mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
  103. mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
  104. mcp_proxy_adapter/examples/run_example.py +59 -0
  105. mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
  106. mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
  107. mcp_proxy_adapter/examples/run_security_tests.py +544 -0
  108. mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
  109. mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
  110. mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
  111. mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
  112. mcp_proxy_adapter/examples/security_test_client.py +782 -0
  113. mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
  114. mcp_proxy_adapter/examples/test_config.py +148 -0
  115. mcp_proxy_adapter/examples/test_config_generator.py +86 -0
  116. mcp_proxy_adapter/examples/test_examples.py +281 -0
  117. mcp_proxy_adapter/examples/universal_client.py +620 -0
  118. mcp_proxy_adapter/main.py +93 -0
  119. mcp_proxy_adapter/utils/config_generator.py +1008 -0
  120. mcp_proxy_adapter/version.py +5 -2
  121. mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
  122. mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
  123. mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
  124. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
  125. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  126. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  127. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  128. mcp_proxy_adapter/examples/README.md +0 -124
  129. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  130. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  131. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  132. mcp_proxy_adapter/examples/basic_server/config.json +0 -35
  133. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  134. mcp_proxy_adapter/examples/basic_server/server.py +0 -103
  135. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  136. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  137. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
  138. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  139. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  140. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  141. mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
  142. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  143. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  144. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  145. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  146. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  147. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  148. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  149. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  150. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  151. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  152. mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
  153. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  154. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  155. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  156. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  157. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  158. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  159. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  160. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  161. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  162. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  163. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  164. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  165. mcp_proxy_adapter/tests/__init__.py +0 -0
  166. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  167. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  168. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  169. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  170. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  171. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  172. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  173. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  174. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  175. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  176. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  177. mcp_proxy_adapter/tests/conftest.py +0 -131
  178. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  179. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  180. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  181. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  182. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  183. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  184. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  185. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  186. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  187. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  188. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  189. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  190. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  191. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  192. mcp_proxy_adapter/tests/test_config.py +0 -127
  193. mcp_proxy_adapter/tests/test_utils.py +0 -65
  194. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  195. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  196. mcp_proxy_adapter/tests/unit/test_config.py +0 -217
  197. mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
  198. mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
  199. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
  200. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,506 @@
1
+ """
2
+ Key Management Command
3
+
4
+ This module provides commands for key management including generation,
5
+ validation, rotation, backup, and restoration.
6
+
7
+ Author: MCP Proxy Adapter Team
8
+ Version: 1.0.0
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ import shutil
14
+ from typing import Dict, List, Optional, Any
15
+ from pathlib import Path
16
+ from datetime import datetime
17
+
18
+ from .base import Command
19
+ from .result import CommandResult, SuccessResult, ErrorResult
20
+ from ..core.certificate_utils import CertificateUtils
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class KeyResult:
26
+ """
27
+ Result class for key operations.
28
+
29
+ Contains key information and operation status.
30
+ """
31
+
32
+ def __init__(self, key_path: str, key_type: str, key_size: int,
33
+ created_date: Optional[str] = None, expiry_date: Optional[str] = None,
34
+ status: str = "valid", error: Optional[str] = None):
35
+ """
36
+ Initialize key result.
37
+
38
+ Args:
39
+ key_path: Path to key file
40
+ key_type: Type of key (RSA, ECDSA, etc.)
41
+ key_size: Key size in bits
42
+ created_date: Key creation date
43
+ expiry_date: Key expiry date
44
+ status: Key status (valid, expired, error)
45
+ error: Error message if any
46
+ """
47
+ self.key_path = key_path
48
+ self.key_type = key_type
49
+ self.key_size = key_size
50
+ self.created_date = created_date
51
+ self.expiry_date = expiry_date
52
+ self.status = status
53
+ self.error = error
54
+
55
+ def to_dict(self) -> Dict[str, Any]:
56
+ """
57
+ Convert to dictionary format.
58
+
59
+ Returns:
60
+ Dictionary representation
61
+ """
62
+ return {
63
+ "key_path": self.key_path,
64
+ "key_type": self.key_type,
65
+ "key_size": self.key_size,
66
+ "created_date": self.created_date,
67
+ "expiry_date": self.expiry_date,
68
+ "status": self.status,
69
+ "error": self.error
70
+ }
71
+
72
+ def get_schema(self) -> Dict[str, Any]:
73
+ """
74
+ Get JSON schema for this result.
75
+
76
+ Returns:
77
+ JSON schema dictionary
78
+ """
79
+ return {
80
+ "type": "object",
81
+ "properties": {
82
+ "key_path": {"type": "string", "description": "Path to key file"},
83
+ "key_type": {"type": "string", "description": "Type of key"},
84
+ "key_size": {"type": "integer", "description": "Key size in bits"},
85
+ "created_date": {"type": "string", "description": "Key creation date"},
86
+ "expiry_date": {"type": "string", "description": "Key expiry date"},
87
+ "status": {"type": "string", "enum": ["valid", "expired", "error"],
88
+ "description": "Key status"},
89
+ "error": {"type": "string", "description": "Error message if any"}
90
+ },
91
+ "required": ["key_path", "key_type", "key_size", "status"]
92
+ }
93
+
94
+
95
+ class KeyManagementCommand(Command):
96
+ """
97
+ Command for key management.
98
+
99
+ Provides methods for generating, validating, rotating, backing up, and restoring keys.
100
+ """
101
+
102
+ # Command metadata
103
+ name = "key_management"
104
+ version = "1.0.0"
105
+ descr = "Private key generation, validation, and management"
106
+ category = "security"
107
+ author = "MCP Proxy Adapter Team"
108
+ email = "team@mcp-proxy-adapter.com"
109
+ source_url = "https://github.com/mcp-proxy-adapter"
110
+ result_class = KeyResult
111
+
112
+ def __init__(self):
113
+ """Initialize key management command."""
114
+ super().__init__()
115
+ self.certificate_utils = CertificateUtils()
116
+
117
+ async def execute(self, **kwargs) -> CommandResult:
118
+ """
119
+ Execute key management command.
120
+
121
+ Args:
122
+ **kwargs: Command parameters including:
123
+ - action: Action to perform (key_generate, key_validate, key_rotate, key_backup, key_restore)
124
+ - key_type: Type of key to generate (RSA, ECDSA)
125
+ - key_size: Key size in bits for generation
126
+ - output_path: Output path for key generation
127
+ - password: Password for key encryption
128
+ - key_path: Key file path for validation, rotation, backup
129
+ - old_key_path: Old key path for rotation
130
+ - new_key_path: New key path for rotation
131
+ - cert_path: Certificate path for rotation
132
+ - backup_old: Whether to backup old key during rotation
133
+ - backup_path: Backup path for key backup/restore
134
+ - encrypt_backup: Whether to encrypt backup
135
+
136
+ Returns:
137
+ CommandResult with key operation status
138
+ """
139
+ action = kwargs.get("action", "key_validate")
140
+
141
+ if action == "key_generate":
142
+ key_type = kwargs.get("key_type")
143
+ key_size = kwargs.get("key_size")
144
+ output_path = kwargs.get("output_path")
145
+ password = kwargs.get("password")
146
+ return await self.key_generate(key_type, key_size, output_path, password)
147
+ elif action == "key_validate":
148
+ key_path = kwargs.get("key_path")
149
+ password = kwargs.get("password")
150
+ return await self.key_validate(key_path, password)
151
+ elif action == "key_rotate":
152
+ old_key_path = kwargs.get("old_key_path")
153
+ new_key_path = kwargs.get("new_key_path")
154
+ cert_path = kwargs.get("cert_path")
155
+ backup_old = kwargs.get("backup_old", True)
156
+ return await self.key_rotate(old_key_path, new_key_path, cert_path, backup_old)
157
+ elif action == "key_backup":
158
+ key_path = kwargs.get("key_path")
159
+ backup_path = kwargs.get("backup_path")
160
+ encrypt_backup = kwargs.get("encrypt_backup", True)
161
+ password = kwargs.get("password")
162
+ return await self.key_backup(key_path, backup_path, encrypt_backup, password)
163
+ elif action == "key_restore":
164
+ backup_path = kwargs.get("backup_path")
165
+ key_path = kwargs.get("key_path")
166
+ password = kwargs.get("password")
167
+ return await self.key_restore(backup_path, key_path, password)
168
+ else:
169
+ return ErrorResult(
170
+ message=f"Unknown action: {action}. Supported actions: key_generate, key_validate, key_rotate, key_backup, key_restore"
171
+ )
172
+
173
+ async def key_generate(self, key_type: str, key_size: int, output_path: str,
174
+ password: Optional[str] = None) -> CommandResult:
175
+ """
176
+ Generate a new private key.
177
+
178
+ Args:
179
+ key_type: Type of key to generate (RSA, ECDSA)
180
+ key_size: Key size in bits
181
+ output_path: Path to save the generated key
182
+ password: Optional password to encrypt the key
183
+
184
+ Returns:
185
+ CommandResult with key generation status
186
+ """
187
+ try:
188
+ logger.info(f"Generating {key_type} key with size {key_size} bits")
189
+
190
+ # Validate parameters
191
+ if key_type not in ["RSA", "ECDSA"]:
192
+ return ErrorResult(
193
+ message="Key type must be RSA or ECDSA"
194
+ )
195
+
196
+ if key_type == "ECDSA":
197
+ if key_size not in [256, 384, 521]:
198
+ return ErrorResult(
199
+ message="ECDSA key size must be 256, 384, or 521 bits"
200
+ )
201
+ elif key_size < 1024:
202
+ return ErrorResult(
203
+ message="Key size must be at least 1024 bits"
204
+ )
205
+
206
+ # Create output directory if it doesn't exist
207
+ output_dir = os.path.dirname(output_path)
208
+ if output_dir:
209
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
210
+
211
+ # Generate key
212
+ result = self.certificate_utils.generate_private_key(
213
+ key_type, key_size, output_path
214
+ )
215
+
216
+ if not result["success"]:
217
+ return ErrorResult(
218
+ message=f"Key generation failed: {result['error']}"
219
+ )
220
+
221
+ key_result = KeyResult(
222
+ key_path=output_path,
223
+ key_type=key_type,
224
+ key_size=key_size,
225
+ created_date=datetime.now().isoformat(),
226
+ status="valid"
227
+ )
228
+
229
+ logger.info(f"Key generated successfully: {output_path}")
230
+ return SuccessResult(
231
+ data={
232
+ "key": key_result.to_dict(),
233
+ "details": result
234
+ }
235
+ )
236
+
237
+ except Exception as e:
238
+ logger.error(f"Key generation failed: {e}")
239
+ return ErrorResult(
240
+ message=f"Key generation failed: {str(e)}"
241
+ )
242
+
243
+ async def key_validate(self, key_path: str, password: Optional[str] = None) -> CommandResult:
244
+ """
245
+ Validate a private key.
246
+
247
+ Args:
248
+ key_path: Path to key file to validate
249
+ password: Optional password if key is encrypted
250
+
251
+ Returns:
252
+ CommandResult with key validation status
253
+ """
254
+ try:
255
+ logger.info(f"Validating key: {key_path}")
256
+
257
+ # Validate parameters
258
+ if not key_path or not os.path.exists(key_path):
259
+ return ErrorResult(
260
+ message=f"Key file not found: {key_path}"
261
+ )
262
+
263
+ # Validate key
264
+ result = self.certificate_utils.validate_private_key(key_path)
265
+
266
+ if not result["success"]:
267
+ return ErrorResult(
268
+ message=f"Key validation failed: {result['error']}"
269
+ )
270
+
271
+ key_result = KeyResult(
272
+ key_path=key_path,
273
+ key_type=result.get("key_type", "unknown"),
274
+ key_size=result.get("key_size", 0),
275
+ created_date=result.get("created_date"),
276
+ status="valid"
277
+ )
278
+
279
+ logger.info(f"Key validation completed: {key_path}")
280
+ return SuccessResult(
281
+ data={
282
+ "key": key_result.to_dict()
283
+ }
284
+ )
285
+
286
+ except Exception as e:
287
+ logger.error(f"Key validation failed: {e}")
288
+ return ErrorResult(
289
+ message=f"Key validation failed: {str(e)}"
290
+ )
291
+
292
+ async def key_rotate(self, old_key_path: str, new_key_path: str,
293
+ cert_path: Optional[str] = None, backup_old: bool = True) -> CommandResult:
294
+ """
295
+ Rotate a private key.
296
+
297
+ Args:
298
+ old_key_path: Path to old key file
299
+ new_key_path: Path to new key file
300
+ cert_path: Optional certificate path to update with new key
301
+ backup_old: Whether to backup the old key
302
+
303
+ Returns:
304
+ CommandResult with key rotation status
305
+ """
306
+ try:
307
+ logger.info(f"Rotating key from {old_key_path} to {new_key_path}")
308
+
309
+ # Validate parameters
310
+ if not old_key_path or not os.path.exists(old_key_path):
311
+ return ErrorResult(
312
+ message=f"Old key file not found: {old_key_path}"
313
+ )
314
+
315
+ if not new_key_path or not os.path.exists(new_key_path):
316
+ return ErrorResult(
317
+ message=f"New key file not found: {new_key_path}"
318
+ )
319
+
320
+ # Validate both keys
321
+ old_key_validation = await self.key_validate(old_key_path)
322
+ new_key_validation = await self.key_validate(new_key_path)
323
+
324
+ if not old_key_validation.to_dict()["success"]:
325
+ return ErrorResult(
326
+ message=f"Old key validation failed: {old_key_validation.to_dict()['error']['message']}"
327
+ )
328
+
329
+ if not new_key_validation.to_dict()["success"]:
330
+ return ErrorResult(
331
+ message=f"New key validation failed: {new_key_validation.to_dict()['error']['message']}"
332
+ )
333
+
334
+ # Backup old key if requested
335
+ backup_path = None
336
+ if backup_old:
337
+ backup_path = f"{old_key_path}.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}"
338
+ shutil.copy2(old_key_path, backup_path)
339
+ logger.info(f"Old key backed up to: {backup_path}")
340
+
341
+ # Update certificate if provided
342
+ cert_updated = False
343
+ if cert_path and os.path.exists(cert_path):
344
+ try:
345
+ # This would require implementing certificate re-signing
346
+ # For now, we'll just note that it needs to be done
347
+ cert_updated = True
348
+ logger.info(f"Certificate {cert_path} needs to be re-signed with new key")
349
+ except Exception as e:
350
+ logger.warning(f"Could not update certificate {cert_path}: {e}")
351
+
352
+ # Replace old key with new key
353
+ shutil.copy2(new_key_path, old_key_path)
354
+
355
+ logger.info(f"Key rotation completed successfully")
356
+ return SuccessResult(
357
+ data={
358
+ "old_key_path": old_key_path,
359
+ "new_key_path": new_key_path,
360
+ "backup_path": backup_path,
361
+ "cert_updated": cert_updated,
362
+ "message": "Key rotation completed successfully"
363
+ }
364
+ )
365
+
366
+ except Exception as e:
367
+ logger.error(f"Key rotation failed: {e}")
368
+ return ErrorResult(
369
+ message=f"Key rotation failed: {str(e)}"
370
+ )
371
+
372
+ async def key_backup(self, key_path: str, backup_path: str,
373
+ encrypt_backup: bool = True, password: Optional[str] = None) -> CommandResult:
374
+ """
375
+ Backup a private key.
376
+
377
+ Args:
378
+ key_path: Path to key file to backup
379
+ backup_path: Path to save the backup
380
+ encrypt_backup: Whether to encrypt the backup
381
+ password: Password for backup encryption
382
+
383
+ Returns:
384
+ CommandResult with backup status
385
+ """
386
+ try:
387
+ logger.info(f"Backing up key: {key_path}")
388
+
389
+ # Validate parameters
390
+ if not key_path or not os.path.exists(key_path):
391
+ return ErrorResult(
392
+ message=f"Key file not found: {key_path}"
393
+ )
394
+
395
+ # Validate key before backup
396
+ key_validation = await self.key_validate(key_path)
397
+ if not key_validation.to_dict()["success"]:
398
+ return ErrorResult(
399
+ message=f"Key validation failed before backup: {key_validation.to_dict()['error']['message']}"
400
+ )
401
+
402
+ # Create backup directory if it doesn't exist
403
+ backup_dir = os.path.dirname(backup_path)
404
+ if backup_dir:
405
+ Path(backup_dir).mkdir(parents=True, exist_ok=True)
406
+
407
+ # Create backup
408
+ if encrypt_backup and password:
409
+ # Encrypted backup
410
+ result = self.certificate_utils.create_encrypted_backup(
411
+ key_path, backup_path, password
412
+ )
413
+ if not result["success"]:
414
+ return ErrorResult(
415
+ message=f"Encrypted backup failed: {result['error']}"
416
+ )
417
+ else:
418
+ # Simple file copy
419
+ shutil.copy2(key_path, backup_path)
420
+
421
+ # Verify backup
422
+ if not os.path.exists(backup_path):
423
+ return ErrorResult(
424
+ message="Backup file was not created"
425
+ )
426
+
427
+ logger.info(f"Key backup completed successfully: {backup_path}")
428
+ return SuccessResult(
429
+ data={
430
+ "key_path": key_path,
431
+ "backup_path": backup_path,
432
+ "encrypted": encrypt_backup,
433
+ "backup_size": os.path.getsize(backup_path),
434
+ "backup_date": datetime.now().isoformat()
435
+ }
436
+ )
437
+
438
+ except Exception as e:
439
+ logger.error(f"Key backup failed: {e}")
440
+ return ErrorResult(
441
+ message=f"Key backup failed: {str(e)}"
442
+ )
443
+
444
+ async def key_restore(self, backup_path: str, key_path: str,
445
+ password: Optional[str] = None) -> CommandResult:
446
+ """
447
+ Restore a private key from backup.
448
+
449
+ Args:
450
+ backup_path: Path to backup file
451
+ key_path: Path to restore the key to
452
+ password: Password if backup is encrypted
453
+
454
+ Returns:
455
+ CommandResult with restore status
456
+ """
457
+ try:
458
+ logger.info(f"Restoring key from backup: {backup_path}")
459
+
460
+ # Validate parameters
461
+ if not backup_path or not os.path.exists(backup_path):
462
+ return ErrorResult(
463
+ message=f"Backup file not found: {backup_path}"
464
+ )
465
+
466
+ # Create target directory if it doesn't exist
467
+ key_dir = os.path.dirname(key_path)
468
+ if key_dir:
469
+ Path(key_dir).mkdir(parents=True, exist_ok=True)
470
+
471
+ # Restore key
472
+ if password:
473
+ # Try encrypted restore first
474
+ result = self.certificate_utils.restore_encrypted_backup(
475
+ backup_path, key_path, password
476
+ )
477
+ if not result["success"]:
478
+ return ErrorResult(
479
+ message=f"Encrypted restore failed: {result['error']}"
480
+ )
481
+ else:
482
+ # Simple file copy
483
+ shutil.copy2(backup_path, key_path)
484
+
485
+ # Validate restored key
486
+ key_validation = await self.key_validate(key_path)
487
+ if not key_validation.to_dict()["success"]:
488
+ return ErrorResult(
489
+ message=f"Restored key validation failed: {key_validation.to_dict()['error']['message']}"
490
+ )
491
+
492
+ logger.info(f"Key restore completed successfully: {key_path}")
493
+ return SuccessResult(
494
+ data={
495
+ "backup_path": backup_path,
496
+ "key_path": key_path,
497
+ "restore_date": datetime.now().isoformat(),
498
+ "key_info": key_validation.to_dict().get("data", {}).get("key") if key_validation.to_dict().get("success") else None
499
+ }
500
+ )
501
+
502
+ except Exception as e:
503
+ logger.error(f"Key restore failed: {e}")
504
+ return ErrorResult(
505
+ message=f"Key restore failed: {str(e)}"
506
+ )
@@ -0,0 +1,176 @@
1
+ """
2
+ Module with load command implementation.
3
+ """
4
+
5
+ from typing import Dict, Any, Optional, List
6
+
7
+ from mcp_proxy_adapter.commands.base import Command
8
+ from mcp_proxy_adapter.commands.result import CommandResult, SuccessResult
9
+ from mcp_proxy_adapter.commands.command_registry import registry
10
+
11
+
12
+ class LoadResult(SuccessResult):
13
+ """
14
+ Result of the load command execution.
15
+ """
16
+
17
+ def __init__(self, success: bool, commands_loaded: int, loaded_commands: list, source: str, error: Optional[str] = None):
18
+ """
19
+ Initialize load command result.
20
+
21
+ Args:
22
+ success: Whether loading was successful
23
+ commands_loaded: Number of commands loaded
24
+ loaded_commands: List of loaded command names
25
+ source: Source path or URL
26
+ error: Error message if loading failed
27
+ """
28
+ data = {
29
+ "success": success,
30
+ "commands_loaded": commands_loaded,
31
+ "loaded_commands": loaded_commands,
32
+ "source": source
33
+ }
34
+ if error:
35
+ data["error"] = error
36
+
37
+ message = f"Loaded {commands_loaded} commands from {source}"
38
+ if error:
39
+ message = f"Failed to load commands from {source}: {error}"
40
+
41
+ super().__init__(data=data, message=message)
42
+
43
+ @classmethod
44
+ def get_schema(cls) -> Dict[str, Any]:
45
+ """
46
+ Get JSON schema for result validation.
47
+
48
+ Returns:
49
+ Dict[str, Any]: JSON schema
50
+ """
51
+ return {
52
+ "type": "object",
53
+ "properties": {
54
+ "data": {
55
+ "type": "object",
56
+ "properties": {
57
+ "success": {"type": "boolean"},
58
+ "commands_loaded": {"type": "integer"},
59
+ "loaded_commands": {
60
+ "type": "array",
61
+ "items": {"type": "string"}
62
+ },
63
+ "source": {"type": "string"},
64
+ "error": {"type": "string"}
65
+ },
66
+ "required": ["success", "commands_loaded", "loaded_commands", "source"]
67
+ }
68
+ },
69
+ "required": ["data"]
70
+ }
71
+
72
+
73
+ class LoadCommand(Command):
74
+ """
75
+ Command that loads commands from local path or URL.
76
+
77
+ This command allows dynamic loading of command modules from either local file system
78
+ or remote HTTP/HTTPS URLs. The command automatically detects whether the source
79
+ is a local path or URL and handles the loading accordingly.
80
+
81
+ For local paths, the command loads Python modules ending with '_command.py'.
82
+ For URLs, the command downloads the Python code and loads it as a temporary module.
83
+
84
+ The loaded commands are registered in the command registry and become immediately
85
+ available for execution. Only commands that inherit from the base Command class
86
+ and are properly structured will be loaded and registered.
87
+
88
+ Security considerations:
89
+ - Local paths are validated for existence and proper naming
90
+ - URLs are downloaded with timeout protection
91
+ - Temporary files are automatically cleaned up after loading
92
+ - Only files ending with '_command.py' are accepted
93
+
94
+ Examples:
95
+ - Load from local file: "./my_command.py"
96
+ - Load from URL: "https://example.com/remote_command.py"
97
+ """
98
+
99
+ name = "load"
100
+ result_class = LoadResult
101
+
102
+ async def execute(self, source: str, **kwargs) -> LoadResult:
103
+ """
104
+ Execute load command.
105
+
106
+ Args:
107
+ source: Source path or URL to load command from
108
+ **kwargs: Additional parameters
109
+
110
+ Returns:
111
+ LoadResult: Load command result
112
+ """
113
+ # Load command from source
114
+ result = registry.load_command_from_source(source)
115
+
116
+ return LoadResult(
117
+ success=result.get("success", False),
118
+ commands_loaded=result.get("commands_loaded", 0),
119
+ loaded_commands=result.get("loaded_commands", []),
120
+ source=result.get("source", source),
121
+ error=result.get("error")
122
+ )
123
+
124
+ @classmethod
125
+ def get_schema(cls) -> Dict[str, Any]:
126
+ """
127
+ Get JSON schema for command parameters.
128
+
129
+ Returns:
130
+ Dict[str, Any]: JSON schema
131
+ """
132
+ return {
133
+ "type": "object",
134
+ "properties": {
135
+ "source": {
136
+ "type": "string",
137
+ "description": "Source path or URL to load command from (must end with '_command.py')",
138
+ "examples": [
139
+ "./my_command.py",
140
+ "https://example.com/remote_command.py"
141
+ ]
142
+ }
143
+ },
144
+ "required": ["source"]
145
+ }
146
+
147
+ @classmethod
148
+ def _generate_examples(cls, params: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
149
+ """
150
+ Generate custom examples for load command.
151
+
152
+ Args:
153
+ params: Information about command parameters
154
+
155
+ Returns:
156
+ List of examples
157
+ """
158
+ examples = [
159
+ {
160
+ "command": cls.name,
161
+ "params": {"source": "./custom_command.py"},
162
+ "description": "Load a command from local file system"
163
+ },
164
+ {
165
+ "command": cls.name,
166
+ "params": {"source": "https://raw.githubusercontent.com/user/repo/main/remote_command.py"},
167
+ "description": "Load a command from GitHub raw content"
168
+ },
169
+ {
170
+ "command": cls.name,
171
+ "params": {"source": "https://example.com/api/commands/test_command.py"},
172
+ "description": "Load a command from remote API endpoint"
173
+ }
174
+ ]
175
+
176
+ return examples