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.
- mcp_proxy_adapter/api/app.py +65 -27
- mcp_proxy_adapter/api/handlers.py +1 -1
- mcp_proxy_adapter/api/middleware/error_handling.py +11 -10
- mcp_proxy_adapter/api/tool_integration.py +5 -2
- mcp_proxy_adapter/api/tools.py +3 -3
- mcp_proxy_adapter/commands/base.py +19 -1
- mcp_proxy_adapter/commands/command_registry.py +243 -6
- mcp_proxy_adapter/commands/hooks.py +260 -0
- mcp_proxy_adapter/commands/reload_command.py +211 -0
- mcp_proxy_adapter/commands/reload_settings_command.py +125 -0
- mcp_proxy_adapter/commands/settings_command.py +189 -0
- mcp_proxy_adapter/config.py +16 -1
- mcp_proxy_adapter/core/__init__.py +44 -0
- mcp_proxy_adapter/core/logging.py +87 -34
- mcp_proxy_adapter/core/settings.py +376 -0
- mcp_proxy_adapter/core/utils.py +2 -2
- mcp_proxy_adapter/custom_openapi.py +81 -2
- mcp_proxy_adapter/examples/README.md +124 -0
- mcp_proxy_adapter/examples/__init__.py +7 -0
- mcp_proxy_adapter/examples/basic_server/README.md +60 -0
- mcp_proxy_adapter/examples/basic_server/__init__.py +7 -0
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +39 -0
- mcp_proxy_adapter/examples/basic_server/config.json +35 -0
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +238 -0
- mcp_proxy_adapter/examples/basic_server/server.py +98 -0
- mcp_proxy_adapter/examples/custom_commands/README.md +127 -0
- mcp_proxy_adapter/examples/custom_commands/__init__.py +27 -0
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +250 -0
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +6 -0
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +103 -0
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +111 -0
- mcp_proxy_adapter/examples/custom_commands/config.json +62 -0
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +169 -0
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +215 -0
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +76 -0
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +96 -0
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +241 -0
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +135 -0
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +122 -0
- mcp_proxy_adapter/examples/custom_commands/hooks.py +230 -0
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +123 -0
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +103 -0
- mcp_proxy_adapter/examples/custom_commands/server.py +223 -0
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +176 -0
- mcp_proxy_adapter/examples/deployment/README.md +49 -0
- mcp_proxy_adapter/examples/deployment/__init__.py +7 -0
- mcp_proxy_adapter/examples/deployment/config.development.json +8 -0
- {examples/basic_example → mcp_proxy_adapter/examples/deployment}/config.json +11 -7
- mcp_proxy_adapter/examples/deployment/config.production.json +12 -0
- mcp_proxy_adapter/examples/deployment/config.staging.json +11 -0
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +31 -0
- mcp_proxy_adapter/examples/deployment/run.sh +43 -0
- mcp_proxy_adapter/examples/deployment/run_docker.sh +84 -0
- mcp_proxy_adapter/openapi.py +3 -2
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +617 -0
- mcp_proxy_adapter/tests/api/test_handlers.py +522 -0
- mcp_proxy_adapter/tests/api/test_schemas.py +546 -0
- mcp_proxy_adapter/tests/api/test_tool_integration.py +531 -0
- mcp_proxy_adapter/tests/unit/test_base_command.py +391 -85
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/METADATA +1 -1
- mcp_proxy_adapter-4.0.0.dist-info/RECORD +110 -0
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/WHEEL +1 -1
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/top_level.txt +0 -1
- examples/__init__.py +0 -19
- examples/anti_patterns/README.md +0 -51
- examples/anti_patterns/__init__.py +0 -9
- examples/anti_patterns/bad_design/README.md +0 -72
- examples/anti_patterns/bad_design/global_state.py +0 -170
- examples/anti_patterns/bad_design/monolithic_command.py +0 -272
- examples/basic_example/README.md +0 -245
- examples/basic_example/__init__.py +0 -8
- examples/basic_example/commands/__init__.py +0 -5
- examples/basic_example/commands/echo_command.py +0 -95
- examples/basic_example/commands/math_command.py +0 -151
- examples/basic_example/commands/time_command.py +0 -152
- examples/basic_example/docs/EN/README.md +0 -177
- examples/basic_example/docs/RU/README.md +0 -177
- examples/basic_example/server.py +0 -151
- examples/basic_example/tests/conftest.py +0 -243
- examples/check_vstl_schema.py +0 -106
- examples/commands/echo_command.py +0 -52
- examples/commands/echo_command_di.py +0 -152
- examples/commands/echo_result.py +0 -65
- examples/commands/get_date_command.py +0 -98
- examples/commands/new_uuid4_command.py +0 -91
- examples/complete_example/Dockerfile +0 -24
- examples/complete_example/README.md +0 -92
- examples/complete_example/__init__.py +0 -8
- examples/complete_example/commands/__init__.py +0 -5
- examples/complete_example/commands/system_command.py +0 -328
- examples/complete_example/config.json +0 -41
- examples/complete_example/configs/config.dev.yaml +0 -40
- examples/complete_example/configs/config.docker.yaml +0 -40
- examples/complete_example/docker-compose.yml +0 -35
- examples/complete_example/requirements.txt +0 -20
- examples/complete_example/server.py +0 -113
- examples/di_example/.pytest_cache/README.md +0 -8
- examples/di_example/server.py +0 -249
- examples/fix_vstl_help.py +0 -123
- examples/minimal_example/README.md +0 -65
- examples/minimal_example/__init__.py +0 -8
- examples/minimal_example/config.json +0 -14
- examples/minimal_example/main.py +0 -136
- examples/minimal_example/simple_server.py +0 -163
- examples/minimal_example/tests/conftest.py +0 -171
- examples/minimal_example/tests/test_hello_command.py +0 -111
- examples/minimal_example/tests/test_integration.py +0 -181
- examples/patch_vstl_service.py +0 -105
- examples/patch_vstl_service_mcp.py +0 -108
- examples/server.py +0 -69
- examples/simple_server.py +0 -128
- examples/test_package_3.1.4.py +0 -177
- examples/test_server.py +0 -134
- examples/tool_description_example.py +0 -82
- mcp_proxy_adapter/py.typed +0 -0
- mcp_proxy_adapter-3.1.6.dist-info/RECORD +0 -118
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.0.0.dist-info}/licenses/LICENSE +0 -0
mcp_proxy_adapter/api/app.py
CHANGED
@@ -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
|
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 (
|
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
|
-
|
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=
|
58
|
-
description=
|
59
|
-
version=
|
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:
|
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
|
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=
|
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":
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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=
|
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.
|
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=
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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
|
mcp_proxy_adapter/api/tools.py
CHANGED
@@ -50,14 +50,14 @@ class TSTCommandExecutor:
|
|
50
50
|
|
51
51
|
try:
|
52
52
|
# Проверяем существование команды
|
53
|
-
if not registry.
|
53
|
+
if not registry.command_exists_with_priority(command):
|
54
54
|
raise NotFoundError(f"Команда '{command}' не найдена")
|
55
55
|
|
56
56
|
# Получаем класс команды
|
57
|
-
command_class = registry.
|
57
|
+
command_class = registry.get_command_with_priority(command)
|
58
58
|
|
59
59
|
# Выполняем команду
|
60
|
-
result = await command_class.
|
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 =
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|