claude-mpm 3.9.7__py3-none-any.whl → 3.9.9__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 (54) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/base_agent.json +1 -1
  3. claude_mpm/agents/templates/ticketing.json +1 -1
  4. claude_mpm/cli/__init__.py +3 -1
  5. claude_mpm/cli/commands/__init__.py +3 -1
  6. claude_mpm/cli/commands/cleanup.py +21 -1
  7. claude_mpm/cli/commands/mcp.py +821 -0
  8. claude_mpm/cli/parser.py +148 -1
  9. claude_mpm/config/memory_guardian_config.py +325 -0
  10. claude_mpm/constants.py +13 -0
  11. claude_mpm/hooks/claude_hooks/hook_handler.py +76 -19
  12. claude_mpm/models/state_models.py +433 -0
  13. claude_mpm/services/__init__.py +28 -0
  14. claude_mpm/services/communication/__init__.py +2 -2
  15. claude_mpm/services/communication/socketio.py +18 -16
  16. claude_mpm/services/infrastructure/__init__.py +4 -1
  17. claude_mpm/services/infrastructure/logging.py +3 -3
  18. claude_mpm/services/infrastructure/memory_guardian.py +770 -0
  19. claude_mpm/services/mcp_gateway/__init__.py +138 -0
  20. claude_mpm/services/mcp_gateway/config/__init__.py +17 -0
  21. claude_mpm/services/mcp_gateway/config/config_loader.py +232 -0
  22. claude_mpm/services/mcp_gateway/config/config_schema.py +234 -0
  23. claude_mpm/services/mcp_gateway/config/configuration.py +371 -0
  24. claude_mpm/services/mcp_gateway/core/__init__.py +51 -0
  25. claude_mpm/services/mcp_gateway/core/base.py +315 -0
  26. claude_mpm/services/mcp_gateway/core/exceptions.py +239 -0
  27. claude_mpm/services/mcp_gateway/core/interfaces.py +476 -0
  28. claude_mpm/services/mcp_gateway/main.py +326 -0
  29. claude_mpm/services/mcp_gateway/registry/__init__.py +12 -0
  30. claude_mpm/services/mcp_gateway/registry/service_registry.py +397 -0
  31. claude_mpm/services/mcp_gateway/registry/tool_registry.py +477 -0
  32. claude_mpm/services/mcp_gateway/server/__init__.py +15 -0
  33. claude_mpm/services/mcp_gateway/server/mcp_server.py +430 -0
  34. claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +444 -0
  35. claude_mpm/services/mcp_gateway/server/stdio_handler.py +373 -0
  36. claude_mpm/services/mcp_gateway/tools/__init__.py +22 -0
  37. claude_mpm/services/mcp_gateway/tools/base_adapter.py +497 -0
  38. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +729 -0
  39. claude_mpm/services/mcp_gateway/tools/hello_world.py +551 -0
  40. claude_mpm/utils/file_utils.py +293 -0
  41. claude_mpm/utils/platform_memory.py +524 -0
  42. claude_mpm/utils/subprocess_utils.py +305 -0
  43. {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/METADATA +4 -1
  44. {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/RECORD +49 -26
  45. claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -36
  46. claude_mpm/agents/templates/.claude-mpm/memories/engineer_agent.md +0 -39
  47. claude_mpm/agents/templates/.claude-mpm/memories/qa_agent.md +0 -38
  48. claude_mpm/agents/templates/.claude-mpm/memories/research_agent.md +0 -39
  49. claude_mpm/agents/templates/.claude-mpm/memories/version_control_agent.md +0 -38
  50. /claude_mpm/agents/templates/{research_memory_efficient.json → backup/research_memory_efficient.json} +0 -0
  51. {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/WHEEL +0 -0
  52. {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/entry_points.txt +0 -0
  53. {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/licenses/LICENSE +0 -0
  54. {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/top_level.txt +0 -0
claude_mpm/cli/parser.py CHANGED
@@ -14,7 +14,7 @@ import argparse
14
14
  from pathlib import Path
15
15
  from typing import Optional, List
16
16
 
17
- from ..constants import CLICommands, CLIPrefix, AgentCommands, MemoryCommands, MonitorCommands, LogLevel, ConfigCommands, AggregateCommands, TicketCommands
17
+ from ..constants import CLICommands, CLIPrefix, AgentCommands, MemoryCommands, MonitorCommands, LogLevel, ConfigCommands, AggregateCommands, TicketCommands, MCPCommands
18
18
 
19
19
 
20
20
  def add_common_arguments(parser: argparse.ArgumentParser, version: str = None) -> None:
@@ -979,6 +979,153 @@ def create_parser(prog_name: str = "claude-mpm", version: str = "0.0.0") -> argp
979
979
  from .commands.cleanup import add_cleanup_parser
980
980
  add_cleanup_parser(subparsers)
981
981
 
982
+ # MCP command with subcommands
983
+ mcp_parser = subparsers.add_parser(
984
+ CLICommands.MCP.value,
985
+ help="Manage MCP Gateway server and tools"
986
+ )
987
+ add_common_arguments(mcp_parser)
988
+
989
+ mcp_subparsers = mcp_parser.add_subparsers(
990
+ dest="mcp_command",
991
+ help="MCP commands",
992
+ metavar="SUBCOMMAND"
993
+ )
994
+
995
+ # Install MCP Gateway
996
+ install_mcp_parser = mcp_subparsers.add_parser(
997
+ MCPCommands.INSTALL.value,
998
+ help="Install and configure MCP Gateway"
999
+ )
1000
+ install_mcp_parser.add_argument(
1001
+ "--force",
1002
+ action="store_true",
1003
+ help="Force overwrite existing configuration"
1004
+ )
1005
+
1006
+ # Start MCP server
1007
+ start_mcp_parser = mcp_subparsers.add_parser(
1008
+ MCPCommands.START.value,
1009
+ help="Start the MCP Gateway server"
1010
+ )
1011
+ start_mcp_parser.add_argument(
1012
+ "--mode",
1013
+ choices=["stdio", "standalone"],
1014
+ default="stdio",
1015
+ help="Server mode: stdio for Claude integration, standalone for testing (default: stdio)"
1016
+ )
1017
+ start_mcp_parser.add_argument(
1018
+ "--port",
1019
+ type=int,
1020
+ default=8766,
1021
+ help="Port for standalone mode (default: 8766)"
1022
+ )
1023
+ start_mcp_parser.add_argument(
1024
+ "--config-file",
1025
+ type=Path,
1026
+ help="Path to MCP configuration file"
1027
+ )
1028
+
1029
+ # Stop MCP server
1030
+ stop_mcp_parser = mcp_subparsers.add_parser(
1031
+ MCPCommands.STOP.value,
1032
+ help="Stop the MCP Gateway server"
1033
+ )
1034
+
1035
+ # MCP status
1036
+ status_mcp_parser = mcp_subparsers.add_parser(
1037
+ MCPCommands.STATUS.value,
1038
+ help="Check server and tool status"
1039
+ )
1040
+ status_mcp_parser.add_argument(
1041
+ "--verbose",
1042
+ action="store_true",
1043
+ help="Show detailed status information"
1044
+ )
1045
+
1046
+ # List/manage tools
1047
+ tools_mcp_parser = mcp_subparsers.add_parser(
1048
+ MCPCommands.TOOLS.value,
1049
+ help="List and manage registered tools"
1050
+ )
1051
+ tools_mcp_parser.add_argument(
1052
+ "tool_action",
1053
+ nargs="?",
1054
+ choices=["list", "enable", "disable"],
1055
+ default="list",
1056
+ help="Tool action (default: list)"
1057
+ )
1058
+ tools_mcp_parser.add_argument(
1059
+ "tool_name",
1060
+ nargs="?",
1061
+ help="Tool name for enable/disable actions"
1062
+ )
1063
+ tools_mcp_parser.add_argument(
1064
+ "--verbose",
1065
+ action="store_true",
1066
+ help="Show detailed tool information including schemas"
1067
+ )
1068
+
1069
+ # Register new tool
1070
+ register_mcp_parser = mcp_subparsers.add_parser(
1071
+ MCPCommands.REGISTER.value,
1072
+ help="Register a new MCP tool"
1073
+ )
1074
+ register_mcp_parser.add_argument(
1075
+ "name",
1076
+ help="Tool name"
1077
+ )
1078
+ register_mcp_parser.add_argument(
1079
+ "description",
1080
+ help="Tool description"
1081
+ )
1082
+ register_mcp_parser.add_argument(
1083
+ "--schema-file",
1084
+ type=Path,
1085
+ help="Path to JSON schema file for tool input"
1086
+ )
1087
+ register_mcp_parser.add_argument(
1088
+ "--adapter",
1089
+ help="Path to custom tool adapter module"
1090
+ )
1091
+ register_mcp_parser.add_argument(
1092
+ "--save",
1093
+ action="store_true",
1094
+ help="Save tool to configuration"
1095
+ )
1096
+
1097
+ # Test tool invocation
1098
+ test_mcp_parser = mcp_subparsers.add_parser(
1099
+ MCPCommands.TEST.value,
1100
+ help="Test MCP tool invocation"
1101
+ )
1102
+ test_mcp_parser.add_argument(
1103
+ "tool_name",
1104
+ help="Name of tool to test"
1105
+ )
1106
+ test_mcp_parser.add_argument(
1107
+ "--args",
1108
+ help="Tool arguments as JSON string"
1109
+ )
1110
+ test_mcp_parser.add_argument(
1111
+ "--args-file",
1112
+ type=Path,
1113
+ help="Path to JSON file containing tool arguments"
1114
+ )
1115
+
1116
+ # Manage configuration
1117
+ config_mcp_parser = mcp_subparsers.add_parser(
1118
+ MCPCommands.CONFIG.value,
1119
+ help="View and manage MCP configuration"
1120
+ )
1121
+ config_mcp_parser.add_argument(
1122
+ "config_action",
1123
+ nargs="?",
1124
+ choices=["view", "edit", "reset"],
1125
+ default="view",
1126
+ help="Configuration action (default: view)"
1127
+ )
1128
+
982
1129
  return parser
983
1130
 
984
1131
 
@@ -0,0 +1,325 @@
1
+ """Memory Guardian configuration for managing Claude Code memory usage.
2
+
3
+ This module provides configuration management for the MemoryGuardian service
4
+ that monitors and manages Claude Code subprocess memory consumption.
5
+
6
+ Design Principles:
7
+ - Platform-agnostic configuration with OS-specific overrides
8
+ - Environment-based configuration for different deployment scenarios
9
+ - Flexible thresholds for memory monitoring
10
+ - Support for both psutil and fallback monitoring methods
11
+ """
12
+
13
+ import os
14
+ import platform
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import Dict, Any, Optional, List
18
+
19
+
20
+ @dataclass
21
+ class MemoryThresholds:
22
+ """Memory threshold configuration in MB."""
23
+
24
+ # Memory thresholds in MB (defaults for 24GB system)
25
+ warning: int = 12288 # 12GB - Start monitoring closely
26
+ critical: int = 15360 # 15GB - Consider restart
27
+ emergency: int = 18432 # 18GB - Force restart
28
+
29
+ # Percentage-based thresholds (as fallback when system memory is detected)
30
+ warning_percent: float = 50.0 # 50% of system memory
31
+ critical_percent: float = 65.0 # 65% of system memory
32
+ emergency_percent: float = 75.0 # 75% of system memory
33
+
34
+ def adjust_for_system_memory(self, total_memory_mb: int) -> None:
35
+ """Adjust thresholds based on available system memory."""
36
+ if total_memory_mb > 0:
37
+ self.warning = int(total_memory_mb * (self.warning_percent / 100))
38
+ self.critical = int(total_memory_mb * (self.critical_percent / 100))
39
+ self.emergency = int(total_memory_mb * (self.emergency_percent / 100))
40
+
41
+ def to_dict(self) -> Dict[str, Any]:
42
+ """Convert thresholds to dictionary."""
43
+ return {
44
+ 'warning_mb': self.warning,
45
+ 'critical_mb': self.critical,
46
+ 'emergency_mb': self.emergency,
47
+ 'warning_percent': self.warning_percent,
48
+ 'critical_percent': self.critical_percent,
49
+ 'emergency_percent': self.emergency_percent
50
+ }
51
+
52
+
53
+ @dataclass
54
+ class RestartPolicy:
55
+ """Configuration for process restart behavior."""
56
+
57
+ # Restart attempts
58
+ max_attempts: int = 3 # Maximum restart attempts before giving up
59
+ attempt_window: int = 3600 # Window in seconds to count attempts (1 hour)
60
+
61
+ # Cooldown periods
62
+ initial_cooldown: int = 30 # Initial cooldown after restart (seconds)
63
+ max_cooldown: int = 300 # Maximum cooldown period (5 minutes)
64
+ cooldown_multiplier: float = 2.0 # Multiply cooldown on each retry
65
+
66
+ # Graceful shutdown
67
+ graceful_timeout: int = 30 # Time to wait for graceful shutdown (seconds)
68
+ force_kill_timeout: int = 10 # Time to wait before SIGKILL after SIGTERM
69
+
70
+ def get_cooldown(self, attempt: int) -> int:
71
+ """Calculate cooldown period for given attempt number."""
72
+ cooldown = self.initial_cooldown * (self.cooldown_multiplier ** (attempt - 1))
73
+ return min(int(cooldown), self.max_cooldown)
74
+
75
+ def to_dict(self) -> Dict[str, Any]:
76
+ """Convert restart policy to dictionary."""
77
+ return {
78
+ 'max_attempts': self.max_attempts,
79
+ 'attempt_window': self.attempt_window,
80
+ 'initial_cooldown': self.initial_cooldown,
81
+ 'max_cooldown': self.max_cooldown,
82
+ 'cooldown_multiplier': self.cooldown_multiplier,
83
+ 'graceful_timeout': self.graceful_timeout,
84
+ 'force_kill_timeout': self.force_kill_timeout
85
+ }
86
+
87
+
88
+ @dataclass
89
+ class MonitoringConfig:
90
+ """Configuration for memory monitoring behavior."""
91
+
92
+ # Check intervals (seconds)
93
+ normal_interval: int = 30 # Normal check interval
94
+ warning_interval: int = 15 # Check interval when in warning state
95
+ critical_interval: int = 5 # Check interval when in critical state
96
+
97
+ # Monitoring method preferences
98
+ prefer_psutil: bool = True # Prefer psutil if available
99
+ fallback_methods: List[str] = field(default_factory=lambda: [
100
+ 'platform_specific', # Use OS-specific commands
101
+ 'resource_module', # Use resource module as last resort
102
+ ])
103
+
104
+ # Logging and reporting
105
+ log_memory_stats: bool = True # Log memory statistics
106
+ log_interval: int = 300 # Log summary every 5 minutes
107
+ detailed_logging: bool = False # Enable detailed debug logging
108
+
109
+ def get_check_interval(self, memory_state: str) -> int:
110
+ """Get check interval based on current memory state."""
111
+ if memory_state == 'critical':
112
+ return self.critical_interval
113
+ elif memory_state == 'warning':
114
+ return self.warning_interval
115
+ else:
116
+ return self.normal_interval
117
+
118
+ def to_dict(self) -> Dict[str, Any]:
119
+ """Convert monitoring config to dictionary."""
120
+ return {
121
+ 'normal_interval': self.normal_interval,
122
+ 'warning_interval': self.warning_interval,
123
+ 'critical_interval': self.critical_interval,
124
+ 'prefer_psutil': self.prefer_psutil,
125
+ 'fallback_methods': self.fallback_methods,
126
+ 'log_memory_stats': self.log_memory_stats,
127
+ 'log_interval': self.log_interval,
128
+ 'detailed_logging': self.detailed_logging
129
+ }
130
+
131
+
132
+ @dataclass
133
+ class PlatformOverrides:
134
+ """Platform-specific configuration overrides."""
135
+
136
+ # macOS specific
137
+ macos_use_activity_monitor: bool = False # Use Activity Monitor data if available
138
+ macos_memory_pressure_check: bool = True # Check system memory pressure
139
+
140
+ # Linux specific
141
+ linux_use_proc: bool = True # Use /proc filesystem
142
+ linux_check_oom_score: bool = True # Monitor OOM killer score
143
+
144
+ # Windows specific
145
+ windows_use_wmi: bool = True # Use WMI for monitoring
146
+ windows_use_performance_counter: bool = False # Use performance counters
147
+
148
+ def to_dict(self) -> Dict[str, Any]:
149
+ """Convert platform overrides to dictionary."""
150
+ return {
151
+ 'macos_use_activity_monitor': self.macos_use_activity_monitor,
152
+ 'macos_memory_pressure_check': self.macos_memory_pressure_check,
153
+ 'linux_use_proc': self.linux_use_proc,
154
+ 'linux_check_oom_score': self.linux_check_oom_score,
155
+ 'windows_use_wmi': self.windows_use_wmi,
156
+ 'windows_use_performance_counter': self.windows_use_performance_counter
157
+ }
158
+
159
+
160
+ @dataclass
161
+ class MemoryGuardianConfig:
162
+ """Complete configuration for MemoryGuardian service."""
163
+
164
+ # Core configurations
165
+ thresholds: MemoryThresholds = field(default_factory=MemoryThresholds)
166
+ restart_policy: RestartPolicy = field(default_factory=RestartPolicy)
167
+ monitoring: MonitoringConfig = field(default_factory=MonitoringConfig)
168
+ platform_overrides: PlatformOverrides = field(default_factory=PlatformOverrides)
169
+
170
+ # Process configuration
171
+ process_command: List[str] = field(default_factory=lambda: ['claude-code'])
172
+ process_args: List[str] = field(default_factory=list)
173
+ process_env: Dict[str, str] = field(default_factory=dict)
174
+ working_directory: Optional[str] = None
175
+
176
+ # Service configuration
177
+ enabled: bool = True # Enable memory guardian
178
+ auto_start: bool = True # Auto-start monitored process
179
+ persist_state: bool = True # Persist state across restarts
180
+ state_file: Optional[str] = None # State file path
181
+
182
+ @classmethod
183
+ def from_env(cls) -> 'MemoryGuardianConfig':
184
+ """Create configuration from environment variables."""
185
+ config = cls()
186
+
187
+ # Memory thresholds
188
+ if warning := os.getenv('CLAUDE_MPM_MEMORY_WARNING_MB'):
189
+ config.thresholds.warning = int(warning)
190
+ if critical := os.getenv('CLAUDE_MPM_MEMORY_CRITICAL_MB'):
191
+ config.thresholds.critical = int(critical)
192
+ if emergency := os.getenv('CLAUDE_MPM_MEMORY_EMERGENCY_MB'):
193
+ config.thresholds.emergency = int(emergency)
194
+
195
+ # Restart policy
196
+ if max_attempts := os.getenv('CLAUDE_MPM_RESTART_MAX_ATTEMPTS'):
197
+ config.restart_policy.max_attempts = int(max_attempts)
198
+ if cooldown := os.getenv('CLAUDE_MPM_RESTART_COOLDOWN'):
199
+ config.restart_policy.initial_cooldown = int(cooldown)
200
+
201
+ # Monitoring intervals
202
+ if interval := os.getenv('CLAUDE_MPM_MONITOR_INTERVAL'):
203
+ config.monitoring.normal_interval = int(interval)
204
+ if log_interval := os.getenv('CLAUDE_MPM_LOG_INTERVAL'):
205
+ config.monitoring.log_interval = int(log_interval)
206
+
207
+ # Service settings
208
+ config.enabled = os.getenv('CLAUDE_MPM_MEMORY_GUARDIAN_ENABLED', 'true').lower() == 'true'
209
+ config.auto_start = os.getenv('CLAUDE_MPM_AUTO_START', 'true').lower() == 'true'
210
+
211
+ # Process command
212
+ if command := os.getenv('CLAUDE_MPM_PROCESS_COMMAND'):
213
+ config.process_command = command.split()
214
+
215
+ return config
216
+
217
+ @classmethod
218
+ def for_development(cls) -> 'MemoryGuardianConfig':
219
+ """Configuration optimized for development."""
220
+ config = cls()
221
+ config.thresholds.warning = 8192 # 8GB for dev machines
222
+ config.thresholds.critical = 10240 # 10GB
223
+ config.thresholds.emergency = 12288 # 12GB
224
+ config.monitoring.normal_interval = 60 # Check less frequently
225
+ config.monitoring.detailed_logging = True
226
+ return config
227
+
228
+ @classmethod
229
+ def for_production(cls) -> 'MemoryGuardianConfig':
230
+ """Configuration optimized for production."""
231
+ config = cls()
232
+ config.monitoring.normal_interval = 30
233
+ config.monitoring.log_memory_stats = True
234
+ config.persist_state = True
235
+ config.restart_policy.max_attempts = 5
236
+ return config
237
+
238
+ @classmethod
239
+ def for_platform(cls, platform_name: Optional[str] = None) -> 'MemoryGuardianConfig':
240
+ """Get platform-specific configuration."""
241
+ if platform_name is None:
242
+ platform_name = platform.system().lower()
243
+
244
+ config = cls()
245
+
246
+ if platform_name == 'darwin': # macOS
247
+ config.platform_overrides.macos_memory_pressure_check = True
248
+ elif platform_name == 'linux':
249
+ config.platform_overrides.linux_use_proc = True
250
+ config.platform_overrides.linux_check_oom_score = True
251
+ elif platform_name == 'windows':
252
+ config.platform_overrides.windows_use_wmi = True
253
+
254
+ return config
255
+
256
+ def to_dict(self) -> Dict[str, Any]:
257
+ """Convert configuration to dictionary."""
258
+ return {
259
+ 'thresholds': self.thresholds.to_dict(),
260
+ 'restart_policy': self.restart_policy.to_dict(),
261
+ 'monitoring': self.monitoring.to_dict(),
262
+ 'platform_overrides': self.platform_overrides.to_dict(),
263
+ 'process_command': self.process_command,
264
+ 'process_args': self.process_args,
265
+ 'process_env': self.process_env,
266
+ 'working_directory': self.working_directory,
267
+ 'enabled': self.enabled,
268
+ 'auto_start': self.auto_start,
269
+ 'persist_state': self.persist_state,
270
+ 'state_file': self.state_file
271
+ }
272
+
273
+ def validate(self) -> List[str]:
274
+ """Validate configuration and return list of issues."""
275
+ issues = []
276
+
277
+ # Validate thresholds are in correct order
278
+ if self.thresholds.warning >= self.thresholds.critical:
279
+ issues.append("Warning threshold must be less than critical threshold")
280
+ if self.thresholds.critical >= self.thresholds.emergency:
281
+ issues.append("Critical threshold must be less than emergency threshold")
282
+
283
+ # Validate intervals
284
+ if self.monitoring.normal_interval <= 0:
285
+ issues.append("Normal monitoring interval must be positive")
286
+ if self.monitoring.warning_interval <= 0:
287
+ issues.append("Warning monitoring interval must be positive")
288
+ if self.monitoring.critical_interval <= 0:
289
+ issues.append("Critical monitoring interval must be positive")
290
+
291
+ # Validate restart policy
292
+ if self.restart_policy.max_attempts < 0:
293
+ issues.append("Max restart attempts cannot be negative")
294
+ if self.restart_policy.initial_cooldown <= 0:
295
+ issues.append("Initial cooldown must be positive")
296
+
297
+ # Validate process command
298
+ if not self.process_command:
299
+ issues.append("Process command cannot be empty")
300
+
301
+ return issues
302
+
303
+
304
+ def get_default_config() -> MemoryGuardianConfig:
305
+ """Get default configuration adjusted for current platform."""
306
+ config = MemoryGuardianConfig.for_platform()
307
+
308
+ # Try to adjust for available system memory
309
+ try:
310
+ import psutil
311
+ total_memory_mb = psutil.virtual_memory().total // (1024 * 1024)
312
+ config.thresholds.adjust_for_system_memory(total_memory_mb)
313
+ except ImportError:
314
+ # psutil not available, use defaults
315
+ pass
316
+ except Exception:
317
+ # Any other error, use defaults
318
+ pass
319
+
320
+ # Override with environment variables
321
+ env_config = MemoryGuardianConfig.from_env()
322
+ if env_config.enabled != config.enabled:
323
+ config = env_config
324
+
325
+ return config
claude_mpm/constants.py CHANGED
@@ -31,6 +31,7 @@ class CLICommands(str, Enum):
31
31
  CONFIG = "config"
32
32
  AGGREGATE = "aggregate"
33
33
  CLEANUP = "cleanup-memory"
34
+ MCP = "mcp"
34
35
 
35
36
  def with_prefix(self, prefix: CLIPrefix = CLIPrefix.MPM) -> str:
36
37
  """Get command with prefix."""
@@ -100,6 +101,18 @@ class AggregateCommands(str, Enum):
100
101
  EXPORT = "export"
101
102
 
102
103
 
104
+ class MCPCommands(str, Enum):
105
+ """MCP Gateway subcommand constants."""
106
+ INSTALL = "install"
107
+ START = "start"
108
+ STOP = "stop"
109
+ STATUS = "status"
110
+ TOOLS = "tools"
111
+ REGISTER = "register"
112
+ TEST = "test"
113
+ CONFIG = "config"
114
+
115
+
103
116
  class TicketCommands(str, Enum):
104
117
  """Ticket subcommand constants."""
105
118
  CREATE = "create"
@@ -16,6 +16,9 @@ import json
16
16
  import sys
17
17
  import os
18
18
  import subprocess
19
+ import signal
20
+ import select
21
+ import atexit
19
22
  from datetime import datetime
20
23
  import time
21
24
  import asyncio
@@ -593,56 +596,93 @@ class ClaudeHookHandler:
593
596
 
594
597
 
595
598
  def handle(self):
596
- """Process hook event with minimal overhead and zero blocking delays.
597
-
599
+ """Process hook event with minimal overhead and timeout protection.
600
+
598
601
  WHY this approach:
599
602
  - Fast path processing for minimal latency (no blocking waits)
600
603
  - Non-blocking Socket.IO connection and event emission
601
- - Removed sleep() delays that were adding 100ms+ to every hook
604
+ - Timeout protection prevents indefinite hangs
602
605
  - Connection timeout prevents indefinite hangs
603
606
  - Graceful degradation if Socket.IO unavailable
604
607
  - Always continues regardless of event status
608
+ - Process exits after handling to prevent accumulation
605
609
  """
610
+ def timeout_handler(signum, frame):
611
+ """Handle timeout by forcing exit."""
612
+ if DEBUG:
613
+ print(f"Hook handler timeout (pid: {os.getpid()})", file=sys.stderr)
614
+ self._continue_execution()
615
+ sys.exit(0)
616
+
606
617
  try:
618
+ # Set a 10-second timeout for the entire operation
619
+ signal.signal(signal.SIGALRM, timeout_handler)
620
+ signal.alarm(10)
621
+
607
622
  # Read and parse event
608
623
  event = self._read_hook_event()
609
624
  if not event:
610
625
  self._continue_execution()
611
626
  return
612
-
627
+
613
628
  # Increment event counter and perform periodic cleanup
614
629
  self.events_processed += 1
615
630
  if self.events_processed % self.CLEANUP_INTERVAL_EVENTS == 0:
616
631
  self._cleanup_old_entries()
617
632
  if DEBUG:
618
633
  print(f"🧹 Performed cleanup after {self.events_processed} events", file=sys.stderr)
619
-
634
+
620
635
  # Route event to appropriate handler
621
636
  self._route_event(event)
622
-
637
+
623
638
  # Always continue execution
624
639
  self._continue_execution()
625
-
640
+
626
641
  except:
627
642
  # Fail fast and silent
628
643
  self._continue_execution()
644
+ finally:
645
+ # Cancel the alarm
646
+ signal.alarm(0)
629
647
 
630
648
  def _read_hook_event(self) -> dict:
631
649
  """
632
- Read and parse hook event from stdin.
633
-
634
- WHY: Centralized event reading with error handling
635
- ensures consistent parsing and validation.
636
-
650
+ Read and parse hook event from stdin with timeout.
651
+
652
+ WHY: Centralized event reading with error handling and timeout
653
+ ensures consistent parsing and validation while preventing
654
+ processes from hanging indefinitely on stdin.read().
655
+
637
656
  Returns:
638
- Parsed event dictionary or None if invalid
657
+ Parsed event dictionary or None if invalid/timeout
639
658
  """
640
659
  try:
660
+ # Check if data is available on stdin with 1 second timeout
661
+ if sys.stdin.isatty():
662
+ # Interactive terminal - no data expected
663
+ return None
664
+
665
+ ready, _, _ = select.select([sys.stdin], [], [], 1.0)
666
+ if not ready:
667
+ # No data available within timeout
668
+ if DEBUG:
669
+ print("No hook event data received within timeout", file=sys.stderr)
670
+ return None
671
+
672
+ # Data is available, read it
641
673
  event_data = sys.stdin.read()
674
+ if not event_data.strip():
675
+ # Empty or whitespace-only data
676
+ return None
677
+
642
678
  return json.loads(event_data)
643
- except (json.JSONDecodeError, ValueError):
679
+ except (json.JSONDecodeError, ValueError) as e:
680
+ if DEBUG:
681
+ print(f"Failed to parse hook event: {e}", file=sys.stderr)
682
+ return None
683
+ except Exception as e:
644
684
  if DEBUG:
645
- print("Failed to parse hook event", file=sys.stderr)
685
+ print(f"Error reading hook event: {e}", file=sys.stderr)
646
686
  return None
647
687
 
648
688
  def _route_event(self, event: dict) -> None:
@@ -1725,9 +1765,22 @@ class ClaudeHookHandler:
1725
1765
 
1726
1766
 
1727
1767
  def main():
1728
- """Entry point with singleton pattern to prevent multiple instances."""
1768
+ """Entry point with singleton pattern and proper cleanup."""
1729
1769
  global _global_handler
1730
-
1770
+
1771
+ def cleanup_handler(signum=None, frame=None):
1772
+ """Cleanup handler for signals and exit."""
1773
+ if DEBUG:
1774
+ print(f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})", file=sys.stderr)
1775
+ # Always output continue action to not block Claude
1776
+ print(json.dumps({"action": "continue"}))
1777
+ sys.exit(0)
1778
+
1779
+ # Register cleanup handlers
1780
+ signal.signal(signal.SIGTERM, cleanup_handler)
1781
+ signal.signal(signal.SIGINT, cleanup_handler)
1782
+ atexit.register(cleanup_handler)
1783
+
1731
1784
  try:
1732
1785
  # Use singleton pattern to prevent creating multiple instances
1733
1786
  with _handler_lock:
@@ -1738,10 +1791,14 @@ def main():
1738
1791
  else:
1739
1792
  if DEBUG:
1740
1793
  print(f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})", file=sys.stderr)
1741
-
1794
+
1742
1795
  handler = _global_handler
1743
-
1796
+
1744
1797
  handler.handle()
1798
+
1799
+ # Ensure we exit after handling
1800
+ cleanup_handler()
1801
+
1745
1802
  except Exception as e:
1746
1803
  # Always output continue action to not block Claude
1747
1804
  print(json.dumps({"action": "continue"}))