mcp-proxy-adapter 3.1.6__py3-none-any.whl → 4.1.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 +254 -8
  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.1.0.dist-info}/METADATA +3 -3
  62. mcp_proxy_adapter-4.1.0.dist-info/RECORD +110 -0
  63. {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/WHEEL +1 -1
  64. {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.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.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,19 +248,29 @@ 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
- def discover_commands(self, package_path: str = "mcp_proxy_adapter.commands") -> None:
260
+ def discover_commands(self, package_path: str = "mcp_proxy_adapter.commands") -> int:
250
261
  """
251
262
  Automatically discovers and registers commands in the specified package.
252
263
 
253
264
  Args:
254
265
  package_path: Path to package with commands.
266
+
267
+ Returns:
268
+ Number of commands discovered and registered.
255
269
  """
256
270
  logger.info(f"Discovering commands in package: {package_path}")
257
271
 
272
+ commands_discovered = 0
273
+
258
274
  try:
259
275
  package = importlib.import_module(package_path)
260
276
  package_dir = os.path.dirname(package.__file__ or "")
@@ -262,7 +278,7 @@ class CommandRegistry:
262
278
  for _, module_name, is_pkg in pkgutil.iter_modules([package_dir]):
263
279
  if is_pkg:
264
280
  # Recursively traverse subpackages
265
- self.discover_commands(f"{package_path}.{module_name}")
281
+ commands_discovered += self.discover_commands(f"{package_path}.{module_name}")
266
282
  elif module_name.endswith("_command"):
267
283
  # Import only command modules
268
284
  module_path = f"{package_path}.{module_name}"
@@ -286,6 +302,8 @@ class CommandRegistry:
286
302
  # Register the command only if it doesn't exist
287
303
  if not self.command_exists(command_name):
288
304
  self.register(cast(Type[Command], obj))
305
+ commands_discovered += 1
306
+ logger.debug(f"Registered command: {command_name}")
289
307
  else:
290
308
  logger.debug(f"Command '{command_name}' is already registered, skipping")
291
309
  except ValueError as e:
@@ -296,13 +314,241 @@ class CommandRegistry:
296
314
  except Exception as e:
297
315
  logger.error(f"Error discovering commands: {e}")
298
316
 
317
+ return commands_discovered
318
+
319
+ def register_custom_command(self, command: Union[Type[Command], Command]) -> None:
320
+ """
321
+ Register a custom command with priority over built-in commands.
322
+
323
+ Args:
324
+ command: Command class or instance to register.
325
+
326
+ Raises:
327
+ ValueError: If command with the same name is already registered.
328
+ """
329
+ # Determine if this is a class or an instance
330
+ if isinstance(command, type) and issubclass(command, Command):
331
+ command_class = command
332
+ command_instance = None
333
+ elif isinstance(command, Command):
334
+ command_class = command.__class__
335
+ command_instance = command
336
+ else:
337
+ raise ValueError(f"Invalid command type: {type(command)}. Expected Command class or instance.")
338
+
339
+ # Get command name
340
+ if not hasattr(command_class, "name") or not command_class.name:
341
+ # Use class name if name attribute is not set
342
+ command_name = command_class.__name__.lower()
343
+ if command_name.endswith("command"):
344
+ command_name = command_name[:-7] # Remove "command" suffix
345
+ else:
346
+ command_name = command_class.name
347
+
348
+ if command_name in self._custom_commands:
349
+ logger.debug(f"Custom command '{command_name}' is already registered, skipping")
350
+ raise ValueError(f"Custom command '{command_name}' is already registered")
351
+
352
+ logger.debug(f"Registering custom command: {command_name}")
353
+ self._custom_commands[command_name] = command_class
354
+
355
+ # Store instance if provided
356
+ if command_instance:
357
+ logger.debug(f"Storing custom instance for command: {command_name}")
358
+ self._instances[command_name] = command_instance
359
+
360
+ def unregister_custom_command(self, command_name: str) -> None:
361
+ """
362
+ Remove custom command from registry.
363
+
364
+ Args:
365
+ command_name: Command name to remove.
366
+
367
+ Raises:
368
+ NotFoundError: If command is not found.
369
+ """
370
+ if command_name not in self._custom_commands:
371
+ raise NotFoundError(f"Custom command '{command_name}' not found")
372
+
373
+ logger.debug(f"Unregistering custom command: {command_name}")
374
+ del self._custom_commands[command_name]
375
+
376
+ # Also remove from instances if present
377
+ if command_name in self._instances:
378
+ del self._instances[command_name]
379
+
380
+ def custom_command_exists(self, command_name: str) -> bool:
381
+ """
382
+ Check if custom command exists.
383
+
384
+ Args:
385
+ command_name: Command name to check.
386
+
387
+ Returns:
388
+ True if custom command exists, False otherwise.
389
+ """
390
+ return command_name in self._custom_commands
391
+
392
+ def get_custom_command(self, command_name: str) -> Type[Command]:
393
+ """
394
+ Get custom command class.
395
+
396
+ Args:
397
+ command_name: Command name.
398
+
399
+ Returns:
400
+ Command class.
401
+
402
+ Raises:
403
+ NotFoundError: If command is not found.
404
+ """
405
+ if command_name not in self._custom_commands:
406
+ raise NotFoundError(f"Custom command '{command_name}' not found")
407
+ return self._custom_commands[command_name]
408
+
409
+ def get_all_custom_commands(self) -> Dict[str, Type[Command]]:
410
+ """
411
+ Get all custom commands.
412
+
413
+ Returns:
414
+ Dictionary with custom command names as keys and classes as values.
415
+ """
416
+ return self._custom_commands.copy()
417
+
418
+ def get_priority_command(self, command_name: str) -> Optional[Type[Command]]:
419
+ """
420
+ Get command with priority (custom commands first, then built-in).
421
+
422
+ Args:
423
+ command_name: Command name.
424
+
425
+ Returns:
426
+ Command class if found, None otherwise.
427
+ """
428
+ # First check custom commands
429
+ if command_name in self._custom_commands:
430
+ return self._custom_commands[command_name]
431
+
432
+ # Then check built-in commands
433
+ if command_name in self._commands:
434
+ return self._commands[command_name]
435
+
436
+ return None
437
+
438
+ def command_exists_with_priority(self, command_name: str) -> bool:
439
+ """
440
+ Check if command exists (custom or built-in).
441
+
442
+ Args:
443
+ command_name: Command name to check.
444
+
445
+ Returns:
446
+ True if command exists, False otherwise.
447
+ """
448
+ return (command_name in self._custom_commands or
449
+ command_name in self._commands)
450
+
451
+ def get_command_with_priority(self, command_name: str) -> Type[Command]:
452
+ """
453
+ Get command with priority (custom commands first, then built-in).
454
+
455
+ Args:
456
+ command_name: Command name.
457
+
458
+ Returns:
459
+ Command class.
460
+
461
+ Raises:
462
+ NotFoundError: If command is not found.
463
+ """
464
+ # First check custom commands
465
+ if command_name in self._custom_commands:
466
+ return self._custom_commands[command_name]
467
+
468
+ # Then check built-in commands
469
+ if command_name in self._commands:
470
+ return self._commands[command_name]
471
+
472
+ raise NotFoundError(f"Command '{command_name}' not found")
473
+
299
474
  def clear(self) -> None:
300
475
  """
301
- Clears command registry.
476
+ Clear all registered commands.
302
477
  """
303
- logger.debug("Clearing command registry")
478
+ logger.debug("Clearing all registered commands")
304
479
  self._commands.clear()
305
480
  self._instances.clear()
481
+ self._custom_commands.clear()
482
+
483
+ def reload_config_and_commands(self, package_path: str = "mcp_proxy_adapter.commands") -> Dict[str, Any]:
484
+ """
485
+ Reload configuration and rediscover commands.
486
+
487
+ Args:
488
+ package_path: Path to package with commands.
489
+
490
+ Returns:
491
+ Dictionary with reload information including:
492
+ - config_reloaded: Whether config was reloaded
493
+ - commands_discovered: Number of commands discovered
494
+ - custom_commands_preserved: Number of custom commands preserved
495
+ - total_commands: Total number of commands after reload
496
+ """
497
+ logger.info("🔄 Starting configuration and commands reload...")
498
+
499
+ # Store current custom commands
500
+ custom_commands_backup = self._custom_commands.copy()
501
+
502
+ # Reload configuration
503
+ try:
504
+ from mcp_proxy_adapter.config import config
505
+ config.load_config()
506
+ config_reloaded = True
507
+ logger.info("✅ Configuration reloaded successfully")
508
+ except Exception as e:
509
+ logger.error(f"❌ Failed to reload configuration: {e}")
510
+ config_reloaded = False
511
+
512
+ # Reinitialize logging with new configuration
513
+ try:
514
+ from mcp_proxy_adapter.core.logging import setup_logging
515
+ setup_logging()
516
+ logger.info("✅ Logging reinitialized with new configuration")
517
+ except Exception as e:
518
+ logger.error(f"❌ Failed to reinitialize logging: {e}")
519
+
520
+ # Clear all commands except custom ones
521
+ self._commands.clear()
522
+ self._instances.clear()
523
+
524
+ # Restore custom commands
525
+ self._custom_commands = custom_commands_backup
526
+ custom_commands_preserved = len(custom_commands_backup)
527
+
528
+ # Rediscover commands
529
+ try:
530
+ commands_discovered = self.discover_commands(package_path)
531
+ logger.info(f"✅ Rediscovered {commands_discovered} commands")
532
+ except Exception as e:
533
+ logger.error(f"❌ Failed to rediscover commands: {e}")
534
+ commands_discovered = 0
535
+
536
+ # Get final counts
537
+ total_commands = len(self._commands)
538
+ built_in_commands = total_commands - custom_commands_preserved
539
+ custom_commands = custom_commands_preserved
540
+
541
+ result = {
542
+ "config_reloaded": config_reloaded,
543
+ "commands_discovered": commands_discovered,
544
+ "custom_commands_preserved": custom_commands_preserved,
545
+ "total_commands": total_commands,
546
+ "built_in_commands": built_in_commands,
547
+ "custom_commands": custom_commands
548
+ }
549
+
550
+ logger.info(f"🔄 Reload completed: {result}")
551
+ return result
306
552
 
307
553
 
308
554
  # Global command registry instance
@@ -0,0 +1,260 @@
1
+ """
2
+ Module for command execution hooks.
3
+
4
+ This module provides a hook system that allows intercepting command execution
5
+ before and after the actual command runs.
6
+ """
7
+
8
+ from typing import Any, Callable, Dict, List, Optional, Union
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+
12
+ from mcp_proxy_adapter.core.logging import logger
13
+
14
+
15
+ class HookType(Enum):
16
+ """Types of hooks."""
17
+ BEFORE_EXECUTION = "before_execution"
18
+ AFTER_EXECUTION = "after_execution"
19
+
20
+
21
+ @dataclass
22
+ class HookContext:
23
+ """Context passed to hook functions."""
24
+ command_name: str
25
+ params: Dict[str, Any]
26
+ hook_type: HookType
27
+ standard_processing: bool = True
28
+ result: Optional[Any] = None
29
+
30
+
31
+ class CommandHooks:
32
+ """
33
+ Manages command execution hooks.
34
+ """
35
+
36
+ def __init__(self):
37
+ """Initialize hooks manager."""
38
+ self._before_hooks: Dict[str, List[Callable]] = {}
39
+ self._after_hooks: Dict[str, List[Callable]] = {}
40
+ self._global_before_hooks: List[Callable] = []
41
+ self._global_after_hooks: List[Callable] = []
42
+
43
+ def register_before_hook(self, command_name: str, hook: Callable[[HookContext], None]) -> None:
44
+ """
45
+ Register a hook to be executed before command execution.
46
+
47
+ Args:
48
+ command_name: Name of the command to hook into
49
+ hook: Hook function that takes HookContext as parameter
50
+ """
51
+ if command_name not in self._before_hooks:
52
+ self._before_hooks[command_name] = []
53
+ self._before_hooks[command_name].append(hook)
54
+ logger.debug(f"Registered before hook for command: {command_name}")
55
+
56
+ def register_after_hook(self, command_name: str, hook: Callable[[HookContext], None]) -> None:
57
+ """
58
+ Register a hook to be executed after command execution.
59
+
60
+ Args:
61
+ command_name: Name of the command to hook into
62
+ hook: Hook function that takes HookContext as parameter
63
+ """
64
+ if command_name not in self._after_hooks:
65
+ self._after_hooks[command_name] = []
66
+ self._after_hooks[command_name].append(hook)
67
+ logger.debug(f"Registered after hook for command: {command_name}")
68
+
69
+ def register_global_before_hook(self, hook: Callable[[HookContext], None]) -> None:
70
+ """
71
+ Register a global hook to be executed before any command.
72
+
73
+ Args:
74
+ hook: Hook function that takes HookContext as parameter
75
+ """
76
+ self._global_before_hooks.append(hook)
77
+ logger.debug("Registered global before hook")
78
+
79
+ def register_global_after_hook(self, hook: Callable[[HookContext], None]) -> None:
80
+ """
81
+ Register a global hook to be executed after any command.
82
+
83
+ Args:
84
+ hook: Hook function that takes HookContext as parameter
85
+ """
86
+ self._global_after_hooks.append(hook)
87
+ logger.debug("Registered global after hook")
88
+
89
+ def unregister_before_hook(self, command_name: str, hook: Callable[[HookContext], None]) -> None:
90
+ """
91
+ Unregister a before hook for a specific command.
92
+
93
+ Args:
94
+ command_name: Name of the command
95
+ hook: Hook function to unregister
96
+ """
97
+ if command_name in self._before_hooks:
98
+ try:
99
+ self._before_hooks[command_name].remove(hook)
100
+ logger.debug(f"Unregistered before hook for command: {command_name}")
101
+ # Remove the command key if no hooks remain
102
+ if not self._before_hooks[command_name]:
103
+ del self._before_hooks[command_name]
104
+ except ValueError:
105
+ logger.warning(f"Hook not found for command: {command_name}")
106
+
107
+ def unregister_after_hook(self, command_name: str, hook: Callable[[HookContext], None]) -> None:
108
+ """
109
+ Unregister an after hook for a specific command.
110
+
111
+ Args:
112
+ command_name: Name of the command
113
+ hook: Hook function to unregister
114
+ """
115
+ if command_name in self._after_hooks:
116
+ try:
117
+ self._after_hooks[command_name].remove(hook)
118
+ logger.debug(f"Unregistered after hook for command: {command_name}")
119
+ # Remove the command key if no hooks remain
120
+ if not self._after_hooks[command_name]:
121
+ del self._after_hooks[command_name]
122
+ except ValueError:
123
+ logger.warning(f"Hook not found for command: {command_name}")
124
+
125
+ def unregister_global_before_hook(self, hook: Callable[[HookContext], None]) -> None:
126
+ """
127
+ Unregister a global before hook.
128
+
129
+ Args:
130
+ hook: Hook function to unregister
131
+ """
132
+ try:
133
+ self._global_before_hooks.remove(hook)
134
+ logger.debug("Unregistered global before hook")
135
+ except ValueError:
136
+ logger.warning("Global before hook not found")
137
+
138
+ def unregister_global_after_hook(self, hook: Callable[[HookContext], None]) -> None:
139
+ """
140
+ Unregister a global after hook.
141
+
142
+ Args:
143
+ hook: Hook function to unregister
144
+ """
145
+ try:
146
+ self._global_after_hooks.remove(hook)
147
+ logger.debug("Unregistered global after hook")
148
+ except ValueError:
149
+ logger.warning("Global after hook not found")
150
+
151
+ def execute_before_hooks(self, command_name: str, params: Dict[str, Any]) -> HookContext:
152
+ """
153
+ Execute all before hooks for a command.
154
+
155
+ Args:
156
+ command_name: Name of the command
157
+ params: Command parameters
158
+
159
+ Returns:
160
+ HookContext with execution results
161
+ """
162
+ context = HookContext(
163
+ command_name=command_name,
164
+ params=params,
165
+ hook_type=HookType.BEFORE_EXECUTION
166
+ )
167
+
168
+ # Execute global before hooks
169
+ for hook in self._global_before_hooks:
170
+ try:
171
+ hook(context)
172
+ except Exception as e:
173
+ logger.error(f"Error in global before hook for command {command_name}: {e}")
174
+
175
+ # Execute command-specific before hooks
176
+ if command_name in self._before_hooks:
177
+ for hook in self._before_hooks[command_name]:
178
+ try:
179
+ hook(context)
180
+ except Exception as e:
181
+ logger.error(f"Error in before hook for command {command_name}: {e}")
182
+
183
+ return context
184
+
185
+ def execute_after_hooks(self, command_name: str, params: Dict[str, Any], result: Any) -> HookContext:
186
+ """
187
+ Execute all after hooks for a command.
188
+
189
+ Args:
190
+ command_name: Name of the command
191
+ params: Command parameters
192
+ result: Command execution result
193
+
194
+ Returns:
195
+ HookContext with execution results
196
+ """
197
+ context = HookContext(
198
+ command_name=command_name,
199
+ params=params,
200
+ hook_type=HookType.AFTER_EXECUTION,
201
+ result=result
202
+ )
203
+
204
+ # Execute command-specific after hooks
205
+ if command_name in self._after_hooks:
206
+ for hook in self._after_hooks[command_name]:
207
+ try:
208
+ hook(context)
209
+ except Exception as e:
210
+ logger.error(f"Error in after hook for command {command_name}: {e}")
211
+
212
+ # Execute global after hooks
213
+ for hook in self._global_after_hooks:
214
+ try:
215
+ hook(context)
216
+ except Exception as e:
217
+ logger.error(f"Error in global after hook for command {command_name}: {e}")
218
+
219
+ return context
220
+
221
+ def clear_hooks(self, command_name: Optional[str] = None) -> None:
222
+ """
223
+ Clear all hooks or hooks for a specific command.
224
+
225
+ Args:
226
+ command_name: If provided, clear hooks only for this command.
227
+ If None, clear all hooks.
228
+ """
229
+ if command_name is None:
230
+ # Clear all hooks
231
+ self._before_hooks.clear()
232
+ self._after_hooks.clear()
233
+ self._global_before_hooks.clear()
234
+ self._global_after_hooks.clear()
235
+ logger.debug("Cleared all hooks")
236
+ else:
237
+ # Clear hooks for specific command
238
+ if command_name in self._before_hooks:
239
+ del self._before_hooks[command_name]
240
+ if command_name in self._after_hooks:
241
+ del self._after_hooks[command_name]
242
+ logger.debug(f"Cleared hooks for command: {command_name}")
243
+
244
+ def get_hook_info(self) -> Dict[str, Any]:
245
+ """
246
+ Get information about registered hooks.
247
+
248
+ Returns:
249
+ Dictionary with hook information
250
+ """
251
+ return {
252
+ "before_hooks": {cmd: len(hooks) for cmd, hooks in self._before_hooks.items()},
253
+ "after_hooks": {cmd: len(hooks) for cmd, hooks in self._after_hooks.items()},
254
+ "global_before_hooks": len(self._global_before_hooks),
255
+ "global_after_hooks": len(self._global_after_hooks)
256
+ }
257
+
258
+
259
+ # Global hooks instance
260
+ hooks = CommandHooks()