mcp-proxy-adapter 3.1.6__py3-none-any.whl → 4.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.
Files changed (118) hide show
  1. mcp_proxy_adapter/api/app.py +65 -27
  2. mcp_proxy_adapter/api/handlers.py +1 -1
  3. mcp_proxy_adapter/api/middleware/error_handling.py +11 -10
  4. mcp_proxy_adapter/api/tool_integration.py +5 -2
  5. mcp_proxy_adapter/api/tools.py +3 -3
  6. mcp_proxy_adapter/commands/base.py +19 -1
  7. mcp_proxy_adapter/commands/command_registry.py +243 -6
  8. mcp_proxy_adapter/commands/hooks.py +260 -0
  9. mcp_proxy_adapter/commands/reload_command.py +211 -0
  10. mcp_proxy_adapter/commands/reload_settings_command.py +125 -0
  11. mcp_proxy_adapter/commands/settings_command.py +189 -0
  12. mcp_proxy_adapter/config.py +16 -1
  13. mcp_proxy_adapter/core/__init__.py +44 -0
  14. mcp_proxy_adapter/core/logging.py +87 -34
  15. mcp_proxy_adapter/core/settings.py +376 -0
  16. mcp_proxy_adapter/core/utils.py +2 -2
  17. mcp_proxy_adapter/custom_openapi.py +81 -2
  18. mcp_proxy_adapter/examples/README.md +124 -0
  19. mcp_proxy_adapter/examples/__init__.py +7 -0
  20. mcp_proxy_adapter/examples/basic_server/README.md +60 -0
  21. mcp_proxy_adapter/examples/basic_server/__init__.py +7 -0
  22. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +39 -0
  23. mcp_proxy_adapter/examples/basic_server/config.json +35 -0
  24. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +238 -0
  25. mcp_proxy_adapter/examples/basic_server/server.py +98 -0
  26. mcp_proxy_adapter/examples/custom_commands/README.md +127 -0
  27. mcp_proxy_adapter/examples/custom_commands/__init__.py +27 -0
  28. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +250 -0
  29. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +6 -0
  30. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +103 -0
  31. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +111 -0
  32. mcp_proxy_adapter/examples/custom_commands/config.json +62 -0
  33. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +169 -0
  34. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +215 -0
  35. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +76 -0
  36. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +96 -0
  37. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +241 -0
  38. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +135 -0
  39. mcp_proxy_adapter/examples/custom_commands/echo_command.py +122 -0
  40. mcp_proxy_adapter/examples/custom_commands/hooks.py +230 -0
  41. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +123 -0
  42. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +103 -0
  43. mcp_proxy_adapter/examples/custom_commands/server.py +223 -0
  44. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +176 -0
  45. mcp_proxy_adapter/examples/deployment/README.md +49 -0
  46. mcp_proxy_adapter/examples/deployment/__init__.py +7 -0
  47. mcp_proxy_adapter/examples/deployment/config.development.json +8 -0
  48. {examples/basic_example → mcp_proxy_adapter/examples/deployment}/config.json +11 -7
  49. mcp_proxy_adapter/examples/deployment/config.production.json +12 -0
  50. mcp_proxy_adapter/examples/deployment/config.staging.json +11 -0
  51. mcp_proxy_adapter/examples/deployment/docker-compose.yml +31 -0
  52. mcp_proxy_adapter/examples/deployment/run.sh +43 -0
  53. mcp_proxy_adapter/examples/deployment/run_docker.sh +84 -0
  54. mcp_proxy_adapter/openapi.py +3 -2
  55. mcp_proxy_adapter/tests/api/test_custom_openapi.py +617 -0
  56. mcp_proxy_adapter/tests/api/test_handlers.py +522 -0
  57. mcp_proxy_adapter/tests/api/test_schemas.py +546 -0
  58. mcp_proxy_adapter/tests/api/test_tool_integration.py +531 -0
  59. mcp_proxy_adapter/tests/unit/test_base_command.py +391 -85
  60. mcp_proxy_adapter/version.py +1 -1
  61. {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/METADATA +1 -1
  62. mcp_proxy_adapter-4.0.0.dist-info/RECORD +110 -0
  63. {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/WHEEL +1 -1
  64. {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/top_level.txt +0 -1
  65. examples/__init__.py +0 -19
  66. examples/anti_patterns/README.md +0 -51
  67. examples/anti_patterns/__init__.py +0 -9
  68. examples/anti_patterns/bad_design/README.md +0 -72
  69. examples/anti_patterns/bad_design/global_state.py +0 -170
  70. examples/anti_patterns/bad_design/monolithic_command.py +0 -272
  71. examples/basic_example/README.md +0 -245
  72. examples/basic_example/__init__.py +0 -8
  73. examples/basic_example/commands/__init__.py +0 -5
  74. examples/basic_example/commands/echo_command.py +0 -95
  75. examples/basic_example/commands/math_command.py +0 -151
  76. examples/basic_example/commands/time_command.py +0 -152
  77. examples/basic_example/docs/EN/README.md +0 -177
  78. examples/basic_example/docs/RU/README.md +0 -177
  79. examples/basic_example/server.py +0 -151
  80. examples/basic_example/tests/conftest.py +0 -243
  81. examples/check_vstl_schema.py +0 -106
  82. examples/commands/echo_command.py +0 -52
  83. examples/commands/echo_command_di.py +0 -152
  84. examples/commands/echo_result.py +0 -65
  85. examples/commands/get_date_command.py +0 -98
  86. examples/commands/new_uuid4_command.py +0 -91
  87. examples/complete_example/Dockerfile +0 -24
  88. examples/complete_example/README.md +0 -92
  89. examples/complete_example/__init__.py +0 -8
  90. examples/complete_example/commands/__init__.py +0 -5
  91. examples/complete_example/commands/system_command.py +0 -328
  92. examples/complete_example/config.json +0 -41
  93. examples/complete_example/configs/config.dev.yaml +0 -40
  94. examples/complete_example/configs/config.docker.yaml +0 -40
  95. examples/complete_example/docker-compose.yml +0 -35
  96. examples/complete_example/requirements.txt +0 -20
  97. examples/complete_example/server.py +0 -113
  98. examples/di_example/.pytest_cache/README.md +0 -8
  99. examples/di_example/server.py +0 -249
  100. examples/fix_vstl_help.py +0 -123
  101. examples/minimal_example/README.md +0 -65
  102. examples/minimal_example/__init__.py +0 -8
  103. examples/minimal_example/config.json +0 -14
  104. examples/minimal_example/main.py +0 -136
  105. examples/minimal_example/simple_server.py +0 -163
  106. examples/minimal_example/tests/conftest.py +0 -171
  107. examples/minimal_example/tests/test_hello_command.py +0 -111
  108. examples/minimal_example/tests/test_integration.py +0 -181
  109. examples/patch_vstl_service.py +0 -105
  110. examples/patch_vstl_service_mcp.py +0 -108
  111. examples/server.py +0 -69
  112. examples/simple_server.py +0 -128
  113. examples/test_package_3.1.4.py +0 -177
  114. examples/test_server.py +0 -134
  115. examples/tool_description_example.py +0 -82
  116. mcp_proxy_adapter/py.typed +0 -0
  117. mcp_proxy_adapter-3.1.6.dist-info/RECORD +0 -118
  118. {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -18,7 +18,7 @@ from mcp_proxy_adapter.config import config
18
18
  from mcp_proxy_adapter.core.errors import MicroserviceError, NotFoundError
19
19
  from mcp_proxy_adapter.core.logging import logger, RequestLogger
20
20
  from mcp_proxy_adapter.commands.command_registry import registry
21
- from mcp_proxy_adapter.custom_openapi import custom_openapi
21
+ from mcp_proxy_adapter.custom_openapi import custom_openapi_with_fallback
22
22
 
23
23
 
24
24
  @asynccontextmanager
@@ -29,12 +29,16 @@ async def lifespan(app: FastAPI):
29
29
  # Startup events
30
30
  from mcp_proxy_adapter.commands.command_registry import registry
31
31
  from mcp_proxy_adapter.commands.help_command import HelpCommand
32
+ from mcp_proxy_adapter.commands.health_command import HealthCommand
32
33
 
33
- # Register built-in commands (help should be registered first)
34
+ # Register built-in commands if they don't exist (user can override them)
34
35
  if not registry.command_exists("help"):
35
36
  registry.register(HelpCommand)
36
37
 
37
- # Register other commands
38
+ if not registry.command_exists("health"):
39
+ registry.register(HealthCommand)
40
+
41
+ # Discover and register additional commands automatically
38
42
  registry.discover_commands()
39
43
 
40
44
  logger.info(f"Application started with {len(registry.get_all_commands())} commands registered")
@@ -45,18 +49,28 @@ async def lifespan(app: FastAPI):
45
49
  logger.info("Application shutting down")
46
50
 
47
51
 
48
- def create_app() -> FastAPI:
52
+ def create_app(title: Optional[str] = None, description: Optional[str] = None, version: Optional[str] = None) -> FastAPI:
49
53
  """
50
54
  Creates and configures FastAPI application.
51
55
 
56
+ Args:
57
+ title: Application title (default: "MCP Proxy Adapter")
58
+ description: Application description (default: "JSON-RPC API for interacting with MCP Proxy")
59
+ version: Application version (default: "1.0.0")
60
+
52
61
  Returns:
53
62
  Configured FastAPI application.
54
63
  """
64
+ # Use provided parameters or defaults
65
+ app_title = title or "MCP Proxy Adapter"
66
+ app_description = description or "JSON-RPC API for interacting with MCP Proxy"
67
+ app_version = version or "1.0.0"
68
+
55
69
  # Create application
56
70
  app = FastAPI(
57
- title="MCP Proxy Adapter",
58
- description="JSON-RPC API for interacting with MCP Proxy",
59
- version="1.0.0",
71
+ title=app_title,
72
+ description=app_description,
73
+ version=app_version,
60
74
  docs_url="/docs",
61
75
  redoc_url="/redoc",
62
76
  lifespan=lifespan,
@@ -75,7 +89,7 @@ def create_app() -> FastAPI:
75
89
  setup_middleware(app)
76
90
 
77
91
  # Use custom OpenAPI schema
78
- app.openapi = lambda: custom_openapi(app)
92
+ app.openapi = lambda: custom_openapi_with_fallback(app)
79
93
 
80
94
  # Explicit endpoint for OpenAPI schema
81
95
  @app.get("/openapi.json")
@@ -83,8 +97,8 @@ def create_app() -> FastAPI:
83
97
  """
84
98
  Returns optimized OpenAPI schema compatible with MCP-Proxy.
85
99
  """
86
- return custom_openapi(app)
87
-
100
+ return custom_openapi_with_fallback(app)
101
+
88
102
  # JSON-RPC handler
89
103
  @app.post("/api/jsonrpc", response_model=Union[JsonRpcSuccessResponse, JsonRpcErrorResponse, List[Union[JsonRpcSuccessResponse, JsonRpcErrorResponse]]])
90
104
  async def jsonrpc_endpoint(request: Request, request_data: Union[Dict[str, Any], List[Dict[str, Any]]] = Body(...)):
@@ -119,7 +133,7 @@ def create_app() -> FastAPI:
119
133
  else:
120
134
  # Process single request
121
135
  return await handle_json_rpc(request_data, request_id)
122
-
136
+
123
137
  # Command execution endpoint (/cmd)
124
138
  @app.post("/cmd")
125
139
  async def cmd_endpoint(request: Request, command_data: Dict[str, Any] = Body(...)):
@@ -245,7 +259,7 @@ def create_app() -> FastAPI:
245
259
  }
246
260
  }
247
261
  )
248
-
262
+
249
263
  # Direct command call
250
264
  @app.post("/api/command/{command_name}")
251
265
  async def command_endpoint(request: Request, command_name: str, params: Dict[str, Any] = Body(default={})):
@@ -259,29 +273,50 @@ def create_app() -> FastAPI:
259
273
  result = await execute_command(command_name, params, request_id)
260
274
  return result
261
275
  except MicroserviceError as e:
276
+ # Convert to proper HTTP status code
277
+ status_code = 400 if e.code < 0 else e.code
262
278
  return JSONResponse(
263
- status_code=e.code,
279
+ status_code=status_code,
264
280
  content=e.to_dict()
265
281
  )
266
-
282
+
267
283
  # Server health check
268
284
  @app.get("/health", operation_id="health_check")
269
285
  async def health_endpoint():
270
286
  """
271
- Проверить работоспособность сервиса.
272
-
273
- Возвращает информацию о состоянии сервиса.
287
+ Health check endpoint.
288
+ Returns server status and basic information.
274
289
  """
275
- # Получаем базовую информацию о здоровье сервера
276
- health_data = await get_server_health()
277
-
278
- # Возвращаем информацию в формате, соответствующем схеме
279
- return JSONResponse(content={
290
+ return {
280
291
  "status": "ok",
281
292
  "model": "mcp-proxy-adapter",
282
- "version": config.get("version", "1.0.0")
283
- })
293
+ "version": "1.0.0"
294
+ }
284
295
 
296
+ # Graceful shutdown endpoint
297
+ @app.post("/shutdown")
298
+ async def shutdown_endpoint():
299
+ """
300
+ Graceful shutdown endpoint.
301
+ Triggers server shutdown after completing current requests.
302
+ """
303
+ import asyncio
304
+
305
+ # Schedule shutdown after a short delay to allow response
306
+ async def delayed_shutdown():
307
+ await asyncio.sleep(1)
308
+ # This will trigger the lifespan shutdown event
309
+ import os
310
+ os._exit(0)
311
+
312
+ # Start shutdown task
313
+ asyncio.create_task(delayed_shutdown())
314
+
315
+ return {
316
+ "status": "shutting_down",
317
+ "message": "Server shutdown initiated. New requests will be rejected."
318
+ }
319
+
285
320
  # List of available commands
286
321
  @app.get("/api/commands", response_model=CommandListResponse)
287
322
  async def commands_list_endpoint():
@@ -290,7 +325,7 @@ def create_app() -> FastAPI:
290
325
  """
291
326
  commands = await get_commands_list()
292
327
  return {"commands": commands}
293
-
328
+
294
329
  # Get command information by name
295
330
  @app.get("/api/commands/{command_name}")
296
331
  async def command_info_endpoint(request: Request, command_name: str):
@@ -317,7 +352,7 @@ def create_app() -> FastAPI:
317
352
  }
318
353
  }
319
354
  )
320
-
355
+
321
356
  # Get API tool description
322
357
  @app.get("/api/tools/{tool_name}")
323
358
  async def tool_description_endpoint(tool_name: str, format: Optional[str] = "json"):
@@ -368,7 +403,7 @@ def create_app() -> FastAPI:
368
403
  }
369
404
  }
370
405
  )
371
-
406
+
372
407
  # Execute API tool
373
408
  @app.post("/api/tools/{tool_name}")
374
409
  async def execute_tool_endpoint(tool_name: str, params: Dict[str, Any] = Body(...)):
@@ -408,5 +443,8 @@ def create_app() -> FastAPI:
408
443
  return app
409
444
 
410
445
 
446
+
447
+
448
+
411
449
  # Create global application instance
412
450
  app = create_app()
@@ -43,7 +43,7 @@ async def execute_command(command_name: str, params: Dict[str, Any], request_id:
43
43
  start_time = time.time()
44
44
 
45
45
  # Use Command.run that handles instances with dependencies properly
46
- command_class = registry.get_command(command_name)
46
+ command_class = registry.get_command_with_priority(command_name)
47
47
  result = await command_class.run(**params)
48
48
 
49
49
  execution_time = time.time() - start_time
@@ -35,7 +35,7 @@ class ErrorHandlingMiddleware(BaseMiddleware):
35
35
  except CommandError as e:
36
36
  # Command error
37
37
  request_id = getattr(request.state, "request_id", "unknown")
38
- logger.error(f"[{request_id}] Command error: {str(e)}")
38
+ logger.debug(f"[{request_id}] Command error: {str(e)}")
39
39
 
40
40
  # Проверяем, является ли запрос JSON-RPC
41
41
  is_jsonrpc = self._is_json_rpc_request(request)
@@ -52,7 +52,7 @@ class ErrorHandlingMiddleware(BaseMiddleware):
52
52
  "error": {
53
53
  "code": e.code,
54
54
  "message": str(e),
55
- "data": e.details if hasattr(e, "details") else None
55
+ "data": e.data if hasattr(e, "data") and e.data else None
56
56
  },
57
57
  "id": request_id_jsonrpc
58
58
  }
@@ -67,7 +67,7 @@ class ErrorHandlingMiddleware(BaseMiddleware):
67
67
  except ValidationError as e:
68
68
  # Validation error
69
69
  request_id = getattr(request.state, "request_id", "unknown")
70
- logger.error(f"[{request_id}] Validation error: {str(e)}")
70
+ logger.debug(f"[{request_id}] Validation error: {str(e)}")
71
71
 
72
72
  # Get JSON-RPC request ID if available
73
73
  request_id_jsonrpc = await self._get_json_rpc_id(request)
@@ -81,7 +81,7 @@ class ErrorHandlingMiddleware(BaseMiddleware):
81
81
  "error": {
82
82
  "code": -32602,
83
83
  "message": "Invalid params",
84
- "data": e.details if hasattr(e, "details") else None
84
+ "data": e.data if hasattr(e, "data") and e.data else None
85
85
  },
86
86
  "id": request_id_jsonrpc
87
87
  }
@@ -96,7 +96,7 @@ class ErrorHandlingMiddleware(BaseMiddleware):
96
96
  except MicroserviceError as e:
97
97
  # Other microservice error
98
98
  request_id = getattr(request.state, "request_id", "unknown")
99
- logger.error(f"[{request_id}] Microservice error: {str(e)}")
99
+ logger.debug(f"[{request_id}] Microservice error: {str(e)}")
100
100
 
101
101
  # Get JSON-RPC request ID if available
102
102
  request_id_jsonrpc = await self._get_json_rpc_id(request)
@@ -104,13 +104,13 @@ class ErrorHandlingMiddleware(BaseMiddleware):
104
104
  # If request was JSON-RPC
105
105
  if self._is_json_rpc_request(request):
106
106
  return JSONResponse(
107
- status_code=e.code,
107
+ status_code=400,
108
108
  content={
109
109
  "jsonrpc": "2.0",
110
110
  "error": {
111
111
  "code": -32000,
112
112
  "message": str(e),
113
- "data": e.details if hasattr(e, "details") else None
113
+ "data": e.data if hasattr(e, "data") and e.data else None
114
114
  },
115
115
  "id": request_id_jsonrpc
116
116
  }
@@ -118,14 +118,14 @@ class ErrorHandlingMiddleware(BaseMiddleware):
118
118
 
119
119
  # Regular API error
120
120
  return JSONResponse(
121
- status_code=e.code,
121
+ status_code=400,
122
122
  content=e.to_dict()
123
123
  )
124
124
 
125
125
  except Exception as e:
126
126
  # Unexpected error
127
127
  request_id = getattr(request.state, "request_id", "unknown")
128
- logger.exception(f"[{request_id}] Unexpected error: {str(e)}")
128
+ logger.debug(f"[{request_id}] Unexpected error: {str(e)}")
129
129
 
130
130
  # Get JSON-RPC request ID if available
131
131
  request_id_jsonrpc = await self._get_json_rpc_id(request)
@@ -165,7 +165,8 @@ class ErrorHandlingMiddleware(BaseMiddleware):
165
165
  Returns:
166
166
  True if request is JSON-RPC, False otherwise.
167
167
  """
168
- return request.url.path.endswith("/jsonrpc")
168
+ # Only requests to /api/jsonrpc are JSON-RPC requests
169
+ return request.url.path == "/api/jsonrpc"
169
170
 
170
171
  async def _get_json_rpc_id(self, request: Request) -> Optional[Any]:
171
172
  """
@@ -126,7 +126,7 @@ class ToolIntegration:
126
126
 
127
127
  logger.info(f"Successfully registered tool: {tool_name}")
128
128
  except Exception as e:
129
- logger.error(f"Error registering tool {tool_name}: {e}")
129
+ logger.debug(f"Error registering tool {tool_name}: {e}")
130
130
  results[tool_name] = {
131
131
  "status": "error",
132
132
  "error": str(e)
@@ -149,7 +149,10 @@ class ToolIntegration:
149
149
 
150
150
  # Формируем словарь типов для всех параметров всех команд
151
151
  for cmd_name, cmd_info in commands.items():
152
- for param_name, param_info in cmd_info.get("params", {}).items():
152
+ params = cmd_info.get("params", {})
153
+ if params is None:
154
+ continue
155
+ for param_name, param_info in params.items():
153
156
  param_type = param_info.get("type", "значение")
154
157
 
155
158
  # Преобразуем русские типы в типы JSON Schema
@@ -50,14 +50,14 @@ class TSTCommandExecutor:
50
50
 
51
51
  try:
52
52
  # Проверяем существование команды
53
- if not registry.command_exists(command):
53
+ if not registry.command_exists_with_priority(command):
54
54
  raise NotFoundError(f"Команда '{command}' не найдена")
55
55
 
56
56
  # Получаем класс команды
57
- command_class = registry.get_command(command)
57
+ command_class = registry.get_command_with_priority(command)
58
58
 
59
59
  # Выполняем команду
60
- result = await command_class.run(**params)
60
+ result = await command_class.execute(**params)
61
61
 
62
62
  # Возвращаем результат
63
63
  return result.to_dict()
@@ -13,6 +13,7 @@ from mcp_proxy_adapter.core.errors import (
13
13
  CommandError, InternalError, InvalidParamsError, NotFoundError, ValidationError
14
14
  )
15
15
  from mcp_proxy_adapter.core.logging import logger
16
+ from .hooks import hooks, HookContext
16
17
 
17
18
 
18
19
  T = TypeVar("T", bound=CommandResult)
@@ -136,6 +137,20 @@ class Command(ABC):
136
137
  # Parameters validation
137
138
  validated_params = cls.validate_params(kwargs)
138
139
 
140
+ # Execute before hooks
141
+ hook_context = hooks.execute_before_hooks(command_name, validated_params)
142
+
143
+ # Check if standard processing should be skipped
144
+ if not hook_context.standard_processing:
145
+ logger.debug(f"Standard processing skipped for command {command_name} due to hook")
146
+ # Return the params as result if standard processing is disabled
147
+ return SuccessResult(data=validated_params)
148
+
149
+ # Get command with priority (custom commands first, then built-in)
150
+ priority_command_class = registry.get_priority_command(command_name)
151
+ if priority_command_class is None:
152
+ raise NotFoundError(f"Command '{command_name}' not found")
153
+
139
154
  # Check if we have a registered instance for this command
140
155
  if registry.has_instance(command_name):
141
156
  # Use existing instance with dependencies
@@ -143,9 +158,12 @@ class Command(ABC):
143
158
  result = await command.execute(**validated_params)
144
159
  else:
145
160
  # Create new instance for commands without dependencies
146
- command = cls()
161
+ command = priority_command_class()
147
162
  result = await command.execute(**validated_params)
148
163
 
164
+ # Execute after hooks
165
+ hooks.execute_after_hooks(command_name, validated_params, result)
166
+
149
167
  logger.debug(f"Command {cls.__name__} executed successfully")
150
168
  return result
151
169
  except ValidationError as e:
@@ -41,6 +41,7 @@ class CommandRegistry:
41
41
  """
42
42
  self._commands: Dict[str, Type[Command]] = {}
43
43
  self._instances: Dict[str, Command] = {}
44
+ self._custom_commands: Dict[str, Type[Command]] = {} # Custom commands with priority
44
45
 
45
46
  def register(self, command: Union[Type[Command], Command]) -> None:
46
47
  """
@@ -196,7 +197,7 @@ class CommandRegistry:
196
197
  Raises:
197
198
  NotFoundError: If command is not found.
198
199
  """
199
- command_class = self.get_command(command_name)
200
+ command_class = self.get_command_with_priority(command_name)
200
201
 
201
202
  return {
202
203
  "name": command_name,
@@ -219,7 +220,7 @@ class CommandRegistry:
219
220
  Raises:
220
221
  NotFoundError: If command is not found
221
222
  """
222
- command_class = self.get_command(command_name)
223
+ command_class = self.get_command_with_priority(command_name)
223
224
  return command_class.get_metadata()
224
225
 
225
226
  def get_all_metadata(self) -> Dict[str, Dict[str, Any]]:
@@ -230,8 +231,13 @@ class CommandRegistry:
230
231
  Dict with command names as keys and metadata as values
231
232
  """
232
233
  metadata = {}
233
- for name, command_class in self._commands.items():
234
+ # Add custom commands first (they have priority)
235
+ for name, command_class in self._custom_commands.items():
234
236
  metadata[name] = command_class.get_metadata()
237
+ # Add built-in commands (custom commands will override if same name)
238
+ for name, command_class in self._commands.items():
239
+ if name not in self._custom_commands: # Only add if not overridden by custom
240
+ metadata[name] = command_class.get_metadata()
235
241
  return metadata
236
242
 
237
243
  def get_all_commands_info(self) -> Dict[str, Dict[str, Any]]:
@@ -242,8 +248,13 @@ class CommandRegistry:
242
248
  Dictionary with information about all commands.
243
249
  """
244
250
  commands_info = {}
245
- for name in self._commands:
251
+ # Add custom commands first (they have priority)
252
+ for name in self._custom_commands:
246
253
  commands_info[name] = self.get_command_info(name)
254
+ # Add built-in commands (custom commands will override if same name)
255
+ for name in self._commands:
256
+ if name not in self._custom_commands: # Only add if not overridden by custom
257
+ commands_info[name] = self.get_command_info(name)
247
258
  return commands_info
248
259
 
249
260
  def discover_commands(self, package_path: str = "mcp_proxy_adapter.commands") -> None:
@@ -296,13 +307,239 @@ class CommandRegistry:
296
307
  except Exception as e:
297
308
  logger.error(f"Error discovering commands: {e}")
298
309
 
310
+ def register_custom_command(self, command: Union[Type[Command], Command]) -> None:
311
+ """
312
+ Register a custom command with priority over built-in commands.
313
+
314
+ Args:
315
+ command: Command class or instance to register.
316
+
317
+ Raises:
318
+ ValueError: If command with the same name is already registered.
319
+ """
320
+ # Determine if this is a class or an instance
321
+ if isinstance(command, type) and issubclass(command, Command):
322
+ command_class = command
323
+ command_instance = None
324
+ elif isinstance(command, Command):
325
+ command_class = command.__class__
326
+ command_instance = command
327
+ else:
328
+ raise ValueError(f"Invalid command type: {type(command)}. Expected Command class or instance.")
329
+
330
+ # Get command name
331
+ if not hasattr(command_class, "name") or not command_class.name:
332
+ # Use class name if name attribute is not set
333
+ command_name = command_class.__name__.lower()
334
+ if command_name.endswith("command"):
335
+ command_name = command_name[:-7] # Remove "command" suffix
336
+ else:
337
+ command_name = command_class.name
338
+
339
+ if command_name in self._custom_commands:
340
+ logger.debug(f"Custom command '{command_name}' is already registered, skipping")
341
+ raise ValueError(f"Custom command '{command_name}' is already registered")
342
+
343
+ logger.debug(f"Registering custom command: {command_name}")
344
+ self._custom_commands[command_name] = command_class
345
+
346
+ # Store instance if provided
347
+ if command_instance:
348
+ logger.debug(f"Storing custom instance for command: {command_name}")
349
+ self._instances[command_name] = command_instance
350
+
351
+ def unregister_custom_command(self, command_name: str) -> None:
352
+ """
353
+ Remove custom command from registry.
354
+
355
+ Args:
356
+ command_name: Command name to remove.
357
+
358
+ Raises:
359
+ NotFoundError: If command is not found.
360
+ """
361
+ if command_name not in self._custom_commands:
362
+ raise NotFoundError(f"Custom command '{command_name}' not found")
363
+
364
+ logger.debug(f"Unregistering custom command: {command_name}")
365
+ del self._custom_commands[command_name]
366
+
367
+ # Also remove from instances if present
368
+ if command_name in self._instances:
369
+ del self._instances[command_name]
370
+
371
+ def custom_command_exists(self, command_name: str) -> bool:
372
+ """
373
+ Check if custom command exists.
374
+
375
+ Args:
376
+ command_name: Command name to check.
377
+
378
+ Returns:
379
+ True if custom command exists, False otherwise.
380
+ """
381
+ return command_name in self._custom_commands
382
+
383
+ def get_custom_command(self, command_name: str) -> Type[Command]:
384
+ """
385
+ Get custom command class.
386
+
387
+ Args:
388
+ command_name: Command name.
389
+
390
+ Returns:
391
+ Command class.
392
+
393
+ Raises:
394
+ NotFoundError: If command is not found.
395
+ """
396
+ if command_name not in self._custom_commands:
397
+ raise NotFoundError(f"Custom command '{command_name}' not found")
398
+ return self._custom_commands[command_name]
399
+
400
+ def get_all_custom_commands(self) -> Dict[str, Type[Command]]:
401
+ """
402
+ Get all custom commands.
403
+
404
+ Returns:
405
+ Dictionary with custom command names as keys and classes as values.
406
+ """
407
+ return self._custom_commands.copy()
408
+
409
+ def get_priority_command(self, command_name: str) -> Optional[Type[Command]]:
410
+ """
411
+ Get command with priority (custom commands first, then built-in).
412
+
413
+ Args:
414
+ command_name: Command name.
415
+
416
+ Returns:
417
+ Command class if found, None otherwise.
418
+ """
419
+ # First check custom commands
420
+ if command_name in self._custom_commands:
421
+ return self._custom_commands[command_name]
422
+
423
+ # Then check built-in commands
424
+ if command_name in self._commands:
425
+ return self._commands[command_name]
426
+
427
+ return None
428
+
429
+ def command_exists_with_priority(self, command_name: str) -> bool:
430
+ """
431
+ Check if command exists (custom or built-in).
432
+
433
+ Args:
434
+ command_name: Command name to check.
435
+
436
+ Returns:
437
+ True if command exists, False otherwise.
438
+ """
439
+ return (command_name in self._custom_commands or
440
+ command_name in self._commands)
441
+
442
+ def get_command_with_priority(self, command_name: str) -> Type[Command]:
443
+ """
444
+ Get command with priority (custom commands first, then built-in).
445
+
446
+ Args:
447
+ command_name: Command name.
448
+
449
+ Returns:
450
+ Command class.
451
+
452
+ Raises:
453
+ NotFoundError: If command is not found.
454
+ """
455
+ # First check custom commands
456
+ if command_name in self._custom_commands:
457
+ return self._custom_commands[command_name]
458
+
459
+ # Then check built-in commands
460
+ if command_name in self._commands:
461
+ return self._commands[command_name]
462
+
463
+ raise NotFoundError(f"Command '{command_name}' not found")
464
+
299
465
  def clear(self) -> None:
300
466
  """
301
- Clears command registry.
467
+ Clear all registered commands.
468
+ """
469
+ logger.debug("Clearing all registered commands")
470
+ self._commands.clear()
471
+ self._instances.clear()
472
+ self._custom_commands.clear()
473
+
474
+ def reload_config_and_commands(self, package_path: str = "mcp_proxy_adapter.commands") -> Dict[str, Any]:
475
+ """
476
+ Reload configuration and rediscover commands.
477
+
478
+ Args:
479
+ package_path: Path to package with commands.
480
+
481
+ Returns:
482
+ Dictionary with reload information including:
483
+ - config_reloaded: Whether config was reloaded
484
+ - commands_discovered: Number of commands discovered
485
+ - custom_commands_preserved: Number of custom commands preserved
486
+ - total_commands: Total number of commands after reload
302
487
  """
303
- logger.debug("Clearing command registry")
488
+ logger.info("🔄 Starting configuration and commands reload...")
489
+
490
+ # Store current custom commands
491
+ custom_commands_backup = self._custom_commands.copy()
492
+
493
+ # Reload configuration
494
+ try:
495
+ from mcp_proxy_adapter.config import config
496
+ config.load_config()
497
+ config_reloaded = True
498
+ logger.info("✅ Configuration reloaded successfully")
499
+ except Exception as e:
500
+ logger.error(f"❌ Failed to reload configuration: {e}")
501
+ config_reloaded = False
502
+
503
+ # Reinitialize logging with new configuration
504
+ try:
505
+ from mcp_proxy_adapter.core.logging import setup_logging
506
+ setup_logging()
507
+ logger.info("✅ Logging reinitialized with new configuration")
508
+ except Exception as e:
509
+ logger.error(f"❌ Failed to reinitialize logging: {e}")
510
+
511
+ # Clear all commands except custom ones
304
512
  self._commands.clear()
305
513
  self._instances.clear()
514
+
515
+ # Restore custom commands
516
+ self._custom_commands = custom_commands_backup
517
+ custom_commands_preserved = len(custom_commands_backup)
518
+
519
+ # Rediscover commands
520
+ try:
521
+ commands_discovered = self.discover_commands(package_path)
522
+ logger.info(f"✅ Rediscovered {commands_discovered} commands")
523
+ except Exception as e:
524
+ logger.error(f"❌ Failed to rediscover commands: {e}")
525
+ commands_discovered = 0
526
+
527
+ # Get final counts
528
+ total_commands = len(self._commands)
529
+ built_in_commands = total_commands - custom_commands_preserved
530
+ custom_commands = custom_commands_preserved
531
+
532
+ result = {
533
+ "config_reloaded": config_reloaded,
534
+ "commands_discovered": commands_discovered,
535
+ "custom_commands_preserved": custom_commands_preserved,
536
+ "total_commands": total_commands,
537
+ "built_in_commands": built_in_commands,
538
+ "custom_commands": custom_commands
539
+ }
540
+
541
+ logger.info(f"🔄 Reload completed: {result}")
542
+ return result
306
543
 
307
544
 
308
545
  # Global command registry instance