claude-mpm 5.0.2__py3-none-any.whl → 5.4.3__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (184) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +2002 -0
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +1218 -905
  4. claude_mpm/agents/agent_loader.py +10 -17
  5. claude_mpm/agents/base_agent_loader.py +10 -35
  6. claude_mpm/agents/frontmatter_validator.py +68 -0
  7. claude_mpm/agents/templates/circuit-breakers.md +431 -45
  8. claude_mpm/cli/__init__.py +0 -1
  9. claude_mpm/cli/commands/__init__.py +2 -0
  10. claude_mpm/cli/commands/agent_state_manager.py +67 -23
  11. claude_mpm/cli/commands/agents.py +446 -25
  12. claude_mpm/cli/commands/auto_configure.py +535 -233
  13. claude_mpm/cli/commands/configure.py +1500 -147
  14. claude_mpm/cli/commands/configure_agent_display.py +13 -6
  15. claude_mpm/cli/commands/mpm_init/core.py +158 -1
  16. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  17. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  18. claude_mpm/cli/commands/postmortem.py +401 -0
  19. claude_mpm/cli/commands/run.py +1 -39
  20. claude_mpm/cli/commands/skills.py +322 -19
  21. claude_mpm/cli/commands/summarize.py +413 -0
  22. claude_mpm/cli/executor.py +8 -0
  23. claude_mpm/cli/interactive/agent_wizard.py +302 -195
  24. claude_mpm/cli/parsers/agents_parser.py +137 -0
  25. claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
  26. claude_mpm/cli/parsers/base_parser.py +9 -0
  27. claude_mpm/cli/parsers/skills_parser.py +7 -0
  28. claude_mpm/cli/startup.py +133 -85
  29. claude_mpm/commands/mpm-agents-auto-configure.md +2 -2
  30. claude_mpm/commands/mpm-agents-list.md +2 -2
  31. claude_mpm/commands/mpm-config-view.md +2 -2
  32. claude_mpm/commands/mpm-help.md +3 -0
  33. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  34. claude_mpm/commands/mpm-postmortem.md +123 -0
  35. claude_mpm/commands/mpm-session-resume.md +2 -2
  36. claude_mpm/commands/mpm-ticket-view.md +2 -2
  37. claude_mpm/config/agent_presets.py +312 -82
  38. claude_mpm/config/agent_sources.py +27 -0
  39. claude_mpm/config/skill_presets.py +392 -0
  40. claude_mpm/constants.py +1 -0
  41. claude_mpm/core/claude_runner.py +2 -25
  42. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  43. claude_mpm/core/framework/loaders/file_loader.py +54 -101
  44. claude_mpm/core/interactive_session.py +19 -5
  45. claude_mpm/core/oneshot_session.py +16 -4
  46. claude_mpm/core/output_style_manager.py +173 -43
  47. claude_mpm/core/protocols/__init__.py +23 -0
  48. claude_mpm/core/protocols/runner_protocol.py +103 -0
  49. claude_mpm/core/protocols/session_protocol.py +131 -0
  50. claude_mpm/core/shared/singleton_manager.py +11 -4
  51. claude_mpm/core/socketio_pool.py +3 -3
  52. claude_mpm/core/system_context.py +38 -0
  53. claude_mpm/core/unified_agent_registry.py +134 -16
  54. claude_mpm/core/unified_config.py +22 -0
  55. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  58. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  59. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  63. claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
  64. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
  65. claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
  66. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  72. claude_mpm/models/agent_definition.py +7 -0
  73. claude_mpm/scripts/launch_monitor.py +93 -13
  74. claude_mpm/services/agents/agent_recommendation_service.py +279 -0
  75. claude_mpm/services/agents/cache_git_manager.py +621 -0
  76. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
  77. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +110 -3
  78. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +518 -55
  79. claude_mpm/services/agents/git_source_manager.py +20 -0
  80. claude_mpm/services/agents/sources/git_source_sync_service.py +45 -6
  81. claude_mpm/services/agents/toolchain_detector.py +6 -5
  82. claude_mpm/services/analysis/__init__.py +35 -0
  83. claude_mpm/services/analysis/clone_detector.py +1030 -0
  84. claude_mpm/services/analysis/postmortem_reporter.py +474 -0
  85. claude_mpm/services/analysis/postmortem_service.py +765 -0
  86. claude_mpm/services/command_deployment_service.py +106 -5
  87. claude_mpm/services/core/base.py +7 -2
  88. claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
  89. claude_mpm/services/event_bus/config.py +3 -1
  90. claude_mpm/services/git/git_operations_service.py +8 -8
  91. claude_mpm/services/mcp_config_manager.py +75 -145
  92. claude_mpm/services/mcp_service_verifier.py +6 -3
  93. claude_mpm/services/monitor/daemon.py +37 -10
  94. claude_mpm/services/monitor/daemon_manager.py +134 -21
  95. claude_mpm/services/monitor/server.py +225 -19
  96. claude_mpm/services/project/project_organizer.py +4 -0
  97. claude_mpm/services/runner_configuration_service.py +16 -3
  98. claude_mpm/services/session_management_service.py +16 -4
  99. claude_mpm/services/socketio/event_normalizer.py +15 -1
  100. claude_mpm/services/socketio/server/core.py +160 -21
  101. claude_mpm/services/version_control/git_operations.py +103 -0
  102. claude_mpm/utils/agent_filters.py +261 -0
  103. claude_mpm/utils/gitignore.py +3 -0
  104. claude_mpm/utils/migration.py +372 -0
  105. claude_mpm/utils/progress.py +5 -1
  106. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +69 -84
  107. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +112 -153
  108. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
  109. claude_mpm/dashboard/analysis_runner.py +0 -455
  110. claude_mpm/dashboard/index.html +0 -13
  111. claude_mpm/dashboard/open_dashboard.py +0 -66
  112. claude_mpm/dashboard/static/css/activity.css +0 -1958
  113. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  114. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  115. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  116. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  117. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  118. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  119. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  120. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  121. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  122. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  123. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  124. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  125. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  126. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  127. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  128. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  129. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  130. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  131. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  132. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  133. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  134. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  135. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  136. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  137. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  138. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  139. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  140. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  141. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  142. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  143. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  144. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  145. claude_mpm/dashboard/templates/code_simple.html +0 -153
  146. claude_mpm/dashboard/templates/index.html +0 -606
  147. claude_mpm/dashboard/test_dashboard.html +0 -372
  148. claude_mpm/scripts/mcp_server.py +0 -75
  149. claude_mpm/scripts/mcp_wrapper.py +0 -39
  150. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  151. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  152. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  153. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  154. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  155. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  156. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  157. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  158. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  159. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  160. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -971
  161. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  162. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  163. claude_mpm/services/mcp_gateway/main.py +0 -589
  164. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  165. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  166. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  167. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  168. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  169. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  170. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  171. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  172. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  173. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  174. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  175. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  176. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  177. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  178. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  179. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  180. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  181. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  182. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
  183. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
  184. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
@@ -1,971 +0,0 @@
1
- """
2
- MCP Process Pool Manager
3
- ========================
4
-
5
- Manages a pool of MCP server processes to prevent multiple instances
6
- and reduce startup overhead through connection reuse.
7
-
8
- WHY: MCP vector search servers load 400MB+ indexes on startup causing 11.9s delays.
9
- By maintaining a process pool and reusing connections, we eliminate this overhead.
10
-
11
- DESIGN DECISIONS:
12
- - Singleton process pool shared across all agent invocations
13
- - Pre-warm processes during framework initialization
14
- - Health checks and automatic restart of failed processes
15
- - Graceful shutdown and resource cleanup
16
- """
17
-
18
- import asyncio
19
- import json
20
- import os
21
- import signal
22
- import subprocess
23
- import sys
24
- import threading
25
- import time
26
- from pathlib import Path
27
- from typing import Any, Dict, Optional
28
-
29
- from claude_mpm.config.paths import paths
30
- from claude_mpm.core.logger import get_logger
31
-
32
-
33
- class MCPProcessPool:
34
- """
35
- Manages a pool of MCP server processes for efficient resource utilization.
36
-
37
- WHY: Prevent multiple MCP server instances from being spawned and
38
- reduce startup overhead by reusing existing processes.
39
- """
40
-
41
- _instance: Optional["MCPProcessPool"] = None
42
- _lock = threading.Lock()
43
-
44
- def __new__(cls):
45
- """Singleton pattern implementation."""
46
- with cls._lock:
47
- if cls._instance is None:
48
- cls._instance = super().__new__(cls)
49
- cls._instance._initialized = False
50
- return cls._instance
51
-
52
- def __init__(self):
53
- """Initialize the process pool manager."""
54
- if self._initialized:
55
- return
56
-
57
- self.logger = get_logger("MCPProcessPool")
58
- self._initialized = True
59
-
60
- # Process tracking
61
- self._processes: Dict[str, subprocess.Popen] = {}
62
- self._process_info: Dict[str, Dict] = {}
63
- self._startup_times: Dict[str, float] = {}
64
-
65
- # Configuration
66
- self.max_processes = 3 # Maximum number of pooled processes
67
- self.process_timeout = 300 # 5 minutes idle timeout
68
- self.health_check_interval = 30 # Check process health every 30s
69
-
70
- # Paths
71
- self.pool_dir = paths.claude_mpm_dir_hidden / "mcp" / "pool"
72
- self.pool_dir.mkdir(parents=True, exist_ok=True)
73
-
74
- # Pre-warming flag
75
- self._pre_warmed = False
76
-
77
- # Background health check task
78
- self._health_check_task: Optional[asyncio.Task] = None
79
-
80
- # Setup cleanup handlers
81
- self._setup_cleanup_handlers()
82
-
83
- self.logger.info("MCP Process Pool initialized")
84
-
85
- def _setup_cleanup_handlers(self):
86
- """Setup signal handlers for cleanup on termination."""
87
-
88
- def cleanup_handler(signum, frame):
89
- self.logger.info(f"Received signal {signum}, cleaning up process pool")
90
- self.cleanup_all()
91
-
92
- signal.signal(signal.SIGTERM, cleanup_handler)
93
- signal.signal(signal.SIGINT, cleanup_handler)
94
-
95
- def get_or_create_process(
96
- self, server_name: str, config: Dict
97
- ) -> Optional[subprocess.Popen]:
98
- """
99
- Get an existing process or create a new one for the given server.
100
-
101
- Args:
102
- server_name: Name of the MCP server
103
- config: Server configuration including command and args
104
-
105
- Returns:
106
- Process handle or None if failed
107
- """
108
- start_time = time.time()
109
-
110
- # Check if we have a healthy existing process
111
- if server_name in self._processes:
112
- process = self._processes[server_name]
113
- if self._is_process_healthy(process):
114
- self.logger.info(
115
- f"Reusing existing process for {server_name} (PID: {process.pid})"
116
- )
117
- return process
118
- # Process is dead, clean it up
119
- self.logger.warning(f"Process for {server_name} is dead, cleaning up")
120
- self._cleanup_process(server_name)
121
-
122
- # Check if we've hit the process limit
123
- if len(self._processes) >= self.max_processes:
124
- # Find and clean up the oldest idle process
125
- self._cleanup_oldest_idle_process()
126
-
127
- # Create new process
128
- self.logger.info(f"Creating new process for {server_name}")
129
- process = self._create_process(server_name, config)
130
-
131
- if process:
132
- create_time = time.time() - start_time
133
- self.logger.info(
134
- f"Process created for {server_name} in {create_time:.2f}s (PID: {process.pid})"
135
- )
136
- self._startup_times[server_name] = create_time
137
-
138
- return process
139
-
140
- def _create_process(
141
- self, server_name: str, config: Dict
142
- ) -> Optional[subprocess.Popen]:
143
- """
144
- Create a new MCP server process.
145
-
146
- Args:
147
- server_name: Name of the MCP server
148
- config: Server configuration
149
-
150
- Returns:
151
- Process handle or None if failed
152
- """
153
- try:
154
- # Extract command and args from config
155
- command = config.get("command", "")
156
- args = config.get("args", [])
157
- env = config.get("env", {})
158
- cwd = config.get("cwd")
159
-
160
- # Build full command
161
- full_command = [command, *args]
162
-
163
- # Merge environment variables
164
- process_env = os.environ.copy()
165
- process_env.update(env)
166
-
167
- # Add timing instrumentation
168
- process_env["MCP_STARTUP_TRACKING"] = "1"
169
- process_env["MCP_SERVER_NAME"] = server_name
170
-
171
- # Start the process
172
- process = subprocess.Popen(
173
- full_command,
174
- stdin=subprocess.PIPE,
175
- stdout=subprocess.PIPE,
176
- stderr=subprocess.PIPE,
177
- env=process_env,
178
- cwd=cwd,
179
- bufsize=0, # Unbuffered for real-time communication
180
- )
181
-
182
- # Store process info
183
- self._processes[server_name] = process
184
- self._process_info[server_name] = {
185
- "pid": process.pid,
186
- "started_at": time.time(),
187
- "last_used": time.time(),
188
- "config": config,
189
- }
190
-
191
- # Write process info to file for debugging
192
- info_file = self.pool_dir / f"{server_name}_{process.pid}.json"
193
- with info_file.open("w") as f:
194
- json.dump(self._process_info[server_name], f, indent=2)
195
-
196
- return process
197
-
198
- except Exception as e:
199
- self.logger.error(f"Failed to create process for {server_name}: {e}")
200
- return None
201
-
202
- def _is_process_healthy(self, process: subprocess.Popen) -> bool:
203
- """Check if a process is still running and healthy."""
204
- if process.poll() is not None:
205
- # Process has terminated
206
- return False
207
-
208
- try:
209
- # Send signal 0 to check if process is alive
210
- os.kill(process.pid, 0)
211
- return True
212
- except (OSError, ProcessLookupError):
213
- return False
214
-
215
- def _cleanup_process(self, server_name: str):
216
- """Clean up a specific process."""
217
- if server_name not in self._processes:
218
- return
219
-
220
- process = self._processes[server_name]
221
-
222
- try:
223
- # Try graceful shutdown first
224
- if self._is_process_healthy(process):
225
- process.terminate()
226
- try:
227
- process.wait(timeout=5)
228
- except subprocess.TimeoutExpired:
229
- # Force kill if graceful shutdown fails
230
- process.kill()
231
- process.wait()
232
-
233
- # Remove from tracking
234
- del self._processes[server_name]
235
- del self._process_info[server_name]
236
-
237
- # Clean up info file
238
- for info_file in self.pool_dir.glob(f"{server_name}_*.json"):
239
- info_file.unlink()
240
-
241
- self.logger.info(f"Cleaned up process for {server_name}")
242
-
243
- except Exception as e:
244
- self.logger.warning(f"Error cleaning up process for {server_name}: {e}")
245
-
246
- def _cleanup_oldest_idle_process(self):
247
- """Find and clean up the oldest idle process."""
248
- if not self._process_info:
249
- return
250
-
251
- # Find process with oldest last_used time
252
- oldest_server = min(
253
- self._process_info.keys(),
254
- key=lambda k: self._process_info[k].get("last_used", 0),
255
- )
256
-
257
- self.logger.info(f"Cleaning up oldest idle process: {oldest_server}")
258
- self._cleanup_process(oldest_server)
259
-
260
- async def pre_warm_servers(self, configs: Dict[str, Dict]):
261
- """
262
- Pre-warm MCP servers during framework initialization.
263
-
264
- Args:
265
- configs: Dictionary of server configurations
266
- """
267
- if self._pre_warmed:
268
- self.logger.info("Servers already pre-warmed")
269
- return
270
-
271
- self.logger.info(f"Pre-warming {len(configs)} MCP servers")
272
- start_time = time.time()
273
-
274
- # Start all servers in parallel
275
- for server_name, config in configs.items():
276
- # Only pre-warm critical servers (like vector search)
277
- if "vector" in server_name.lower() or config.get("pre_warm", False):
278
- self.logger.info(f"Pre-warming {server_name}")
279
- process = self.get_or_create_process(server_name, config)
280
- if process:
281
- self.logger.info(f"Pre-warmed {server_name} (PID: {process.pid})")
282
-
283
- self._pre_warmed = True
284
- total_time = time.time() - start_time
285
- self.logger.info(f"Pre-warming completed in {total_time:.2f}s")
286
-
287
- async def start_health_monitoring(self):
288
- """Start background health monitoring of processes."""
289
- if self._health_check_task and not self._health_check_task.done():
290
- return
291
-
292
- self._health_check_task = asyncio.create_task(self._health_check_loop())
293
- self.logger.info("Started health monitoring")
294
-
295
- async def _health_check_loop(self):
296
- """Background loop to check process health."""
297
- while True:
298
- try:
299
- await asyncio.sleep(self.health_check_interval)
300
-
301
- # Check each process
302
- dead_processes = []
303
- for server_name, process in self._processes.items():
304
- if not self._is_process_healthy(process):
305
- dead_processes.append(server_name)
306
-
307
- # Clean up dead processes
308
- for server_name in dead_processes:
309
- self.logger.warning(f"Process {server_name} is dead, cleaning up")
310
- self._cleanup_process(server_name)
311
-
312
- # Check for idle timeout
313
- current_time = time.time()
314
- idle_processes = []
315
- for server_name, info in self._process_info.items():
316
- last_used = info.get("last_used", current_time)
317
- if current_time - last_used > self.process_timeout:
318
- idle_processes.append(server_name)
319
-
320
- # Clean up idle processes
321
- for server_name in idle_processes:
322
- self.logger.info(f"Process {server_name} idle timeout, cleaning up")
323
- self._cleanup_process(server_name)
324
-
325
- except Exception as e:
326
- self.logger.error(f"Error in health check loop: {e}")
327
-
328
- def mark_process_used(self, server_name: str):
329
- """Mark a process as recently used."""
330
- if server_name in self._process_info:
331
- self._process_info[server_name]["last_used"] = time.time()
332
-
333
- def get_startup_metrics(self) -> Dict[str, float]:
334
- """Get startup time metrics for all servers."""
335
- return self._startup_times.copy()
336
-
337
- def get_pool_status(self) -> Dict[str, Any]:
338
- """Get current status of the process pool."""
339
- return {
340
- "active_processes": len(self._processes),
341
- "max_processes": self.max_processes,
342
- "pre_warmed": self._pre_warmed,
343
- "processes": {
344
- name: {
345
- "pid": info.get("pid"),
346
- "uptime": time.time() - info.get("started_at", time.time()),
347
- "idle_time": time.time() - info.get("last_used", time.time()),
348
- }
349
- for name, info in self._process_info.items()
350
- },
351
- "startup_metrics": self._startup_times,
352
- }
353
-
354
- def cleanup_all(self):
355
- """Clean up all processes in the pool."""
356
- self.logger.info("Cleaning up all processes in pool")
357
-
358
- # Stop health monitoring
359
- if self._health_check_task:
360
- self._health_check_task.cancel()
361
-
362
- # Clean up all processes
363
- for server_name in list(self._processes.keys()):
364
- self._cleanup_process(server_name)
365
-
366
- self.logger.info("Process pool cleanup completed")
367
-
368
-
369
- # Global instance
370
- _pool: Optional[MCPProcessPool] = None
371
-
372
-
373
- def get_process_pool() -> MCPProcessPool:
374
- """Get the global MCP process pool instance."""
375
- global _pool
376
- if _pool is None:
377
- _pool = MCPProcessPool()
378
- return _pool
379
-
380
-
381
- async def auto_initialize_vector_search():
382
- """
383
- Auto-initialize mcp-vector-search for the current project.
384
-
385
- WHY: Vector search requires project initialization before it can be used.
386
- This function ensures the current project is automatically initialized
387
- for vector search when the system starts up.
388
-
389
- DESIGN DECISION:
390
- - Automatically install mcp-vector-search if not present
391
- - Run in background with timeout to avoid blocking startup
392
- - Failures are logged but don't prevent the system from starting
393
- """
394
- logger = get_logger("vector_search_init")
395
-
396
- try:
397
- # Import MCPConfigManager to handle installation
398
- from claude_mpm.services.mcp_config_manager import MCPConfigManager
399
-
400
- config_manager = MCPConfigManager()
401
-
402
- # Check if mcp-vector-search is already installed
403
- vector_search_path = config_manager.detect_service_path("mcp-vector-search")
404
-
405
- if vector_search_path:
406
- logger.debug(f"mcp-vector-search found at: {vector_search_path}")
407
- else:
408
- # Not installed - attempt installation
409
- logger.info("šŸ” mcp-vector-search not found. Installing via pipx...")
410
-
411
- # First check if pipx is available
412
- import shutil
413
- import subprocess
414
-
415
- if not shutil.which("pipx"):
416
- logger.warning(
417
- "āš ļø pipx not found. Please install pipx to enable automatic mcp-vector-search installation"
418
- )
419
- logger.info(" Install pipx with: python -m pip install --user pipx")
420
- return
421
-
422
- try:
423
- result = subprocess.run(
424
- ["pipx", "install", "mcp-vector-search"],
425
- capture_output=True,
426
- text=True,
427
- timeout=60,
428
- check=False, # 1 minute timeout for installation
429
- )
430
-
431
- if result.returncode == 0:
432
- logger.info("āœ… mcp-vector-search installed successfully")
433
- # Detect the newly installed path
434
- vector_search_path = config_manager.detect_service_path(
435
- "mcp-vector-search"
436
- )
437
- if not vector_search_path:
438
- logger.warning(
439
- "mcp-vector-search installed but command not found in PATH"
440
- )
441
- return
442
-
443
- # Update the Claude configuration to include the newly installed service
444
- logger.info("šŸ“ Updating Claude configuration...")
445
- config_success, config_msg = (
446
- config_manager.ensure_mcp_services_configured()
447
- )
448
- if config_success:
449
- logger.info(f"āœ… {config_msg}")
450
- else:
451
- logger.warning(f"āš ļø Configuration update issue: {config_msg}")
452
- else:
453
- logger.warning(
454
- f"Failed to install mcp-vector-search: {result.stderr}"
455
- )
456
- return
457
-
458
- except subprocess.TimeoutExpired:
459
- logger.warning("Installation of mcp-vector-search timed out")
460
- return
461
- except Exception as e:
462
- logger.warning(f"Error installing mcp-vector-search: {e}")
463
- return
464
-
465
- # At this point, mcp-vector-search should be available
466
- # Get the actual command to use
467
- import shutil
468
-
469
- vector_search_cmd = shutil.which("mcp-vector-search")
470
- if not vector_search_cmd:
471
- # Try pipx installation path as fallback
472
- pipx_path = (
473
- Path.home()
474
- / ".local/pipx/venvs/mcp-vector-search/bin/mcp-vector-search"
475
- )
476
- if pipx_path.exists():
477
- vector_search_cmd = str(pipx_path)
478
- else:
479
- logger.debug("mcp-vector-search command not found after installation")
480
- return
481
-
482
- # Check if current project is already initialized
483
- current_dir = Path.cwd()
484
- vector_config = current_dir / ".mcp-vector-search/config.json"
485
-
486
- if vector_config.exists():
487
- logger.debug(f"Vector search already initialized for {current_dir}")
488
-
489
- # Ensure .mcp-vector-search is in gitignore even if already initialized
490
- try:
491
- from ....services.project.project_organizer import ProjectOrganizer
492
-
493
- if (current_dir / ".claude-mpm").exists() or (
494
- current_dir / ".git"
495
- ).exists():
496
- organizer = ProjectOrganizer(current_dir)
497
- organizer.update_gitignore(
498
- additional_patterns=[".mcp-vector-search/"]
499
- )
500
- logger.debug("Ensured .mcp-vector-search is in gitignore")
501
- except Exception as e:
502
- logger.debug(f"Could not update gitignore for .mcp-vector-search: {e}")
503
- # Check if index needs rebuilding (corrupted database)
504
- chroma_db = current_dir / ".mcp-vector-search/chroma.sqlite3"
505
- if chroma_db.exists():
506
- # Quick health check - verify database file exists and is accessible
507
- try:
508
- # Check if database file exists and has reasonable size
509
- if chroma_db.exists() and chroma_db.stat().st_size > 0:
510
- logger.info("āœ“ Vector search index is healthy and ready")
511
- return
512
- logger.info("āš ļø Vector search index may be corrupted, rebuilding...")
513
- except Exception as e:
514
- logger.debug(
515
- f"Vector search health check failed: {e}, will attempt to rebuild"
516
- )
517
-
518
- # Initialize or reinitialize the project
519
- logger.info(f"šŸŽÆ Initializing vector search for project: {current_dir}")
520
-
521
- # Initialize the project (this creates the config)
522
- # Note: mcp-vector-search operates on the current directory
523
- import subprocess
524
-
525
- proc = subprocess.run(
526
- [vector_search_cmd, "init"],
527
- capture_output=True,
528
- text=True,
529
- timeout=30,
530
- cwd=str(current_dir),
531
- check=False, # Run in the project directory
532
- )
533
-
534
- if proc.returncode == 0:
535
- logger.info("āœ… Vector search initialization completed")
536
-
537
- # Ensure .mcp-vector-search is in gitignore
538
- try:
539
- from ....services.project.project_organizer import ProjectOrganizer
540
-
541
- # Check if we're in a git repository (parent of .claude-mpm)
542
- if (current_dir / ".claude-mpm").exists() or (
543
- current_dir / ".git"
544
- ).exists():
545
- organizer = ProjectOrganizer(current_dir)
546
- organizer.update_gitignore(
547
- additional_patterns=[".mcp-vector-search/"]
548
- )
549
- logger.debug("Ensured .mcp-vector-search is in gitignore")
550
- except Exception as e:
551
- logger.debug(f"Could not update gitignore for .mcp-vector-search: {e}")
552
- # Non-critical, don't fail initialization
553
-
554
- # Start background indexing (non-blocking)
555
- def background_index():
556
- try:
557
- logger.info("šŸ”„ Starting project indexing in background...")
558
- index_proc = subprocess.run(
559
- [vector_search_cmd, "index", "main"],
560
- capture_output=True,
561
- text=True,
562
- timeout=300, # 5 minute timeout for indexing
563
- cwd=str(current_dir),
564
- check=False, # Run in the project directory
565
- )
566
- if index_proc.returncode == 0:
567
- logger.info("āœ… Project indexing completed successfully")
568
- # Parse output to show statistics if available
569
- if "indexed" in index_proc.stdout.lower():
570
- # Extract and log indexing statistics
571
- lines = index_proc.stdout.strip().split("\n")
572
- for line in lines:
573
- if "indexed" in line.lower() or "files" in line.lower():
574
- logger.info(f" {line.strip()}")
575
- else:
576
- logger.warning(
577
- f"āš ļø Project indexing failed: {index_proc.stderr}"
578
- )
579
- except subprocess.TimeoutExpired:
580
- logger.warning(
581
- "āš ļø Project indexing timed out (will continue in background)"
582
- )
583
- except Exception as e:
584
- logger.debug(f"Background indexing error (non-critical): {e}")
585
-
586
- # Run indexing in background thread
587
- import threading
588
-
589
- index_thread = threading.Thread(target=background_index, daemon=True)
590
- index_thread.start()
591
- logger.info(
592
- "šŸ“š Background indexing started - vector search will be available shortly"
593
- )
594
-
595
- else:
596
- logger.warning(f"āš ļø Vector search initialization failed: {proc.stderr}")
597
-
598
- except Exception as e:
599
- logger.debug(f"Vector search auto-initialization error (non-critical): {e}")
600
-
601
-
602
- async def auto_initialize_kuzu_memory():
603
- """
604
- Auto-initialize kuzu-memory for persistent knowledge storage.
605
-
606
- WHY: Kuzu-memory provides a graph database for structured memory storage
607
- with semantic search capabilities, enabling persistent context across sessions.
608
-
609
- DESIGN DECISION:
610
- - Automatically install kuzu-memory if not present via pipx
611
- - Initialize database in background to avoid blocking startup
612
- - Failures are logged but don't prevent the system from starting
613
- """
614
- logger = get_logger("kuzu_memory_init")
615
-
616
- try:
617
- # Import MCPConfigManager to handle installation
618
- from claude_mpm.services.mcp_config_manager import MCPConfigManager
619
-
620
- config_manager = MCPConfigManager()
621
-
622
- # Check if kuzu-memory is already installed
623
- kuzu_memory_path = config_manager.detect_service_path("kuzu-memory")
624
-
625
- if kuzu_memory_path:
626
- logger.debug(f"kuzu-memory found at: {kuzu_memory_path}")
627
- else:
628
- # Not installed - attempt installation
629
- logger.info("🧠 kuzu-memory not found. Installing via pipx...")
630
-
631
- # First check if pipx is available
632
- import shutil
633
- import subprocess
634
-
635
- if not shutil.which("pipx"):
636
- logger.warning(
637
- "āš ļø pipx not found. Please install pipx to enable automatic kuzu-memory installation"
638
- )
639
- logger.info(" Install pipx with: python -m pip install --user pipx")
640
- return
641
-
642
- try:
643
- result = subprocess.run(
644
- ["pipx", "install", "kuzu-memory"],
645
- capture_output=True,
646
- text=True,
647
- timeout=60,
648
- check=False, # 1 minute timeout for installation
649
- )
650
-
651
- if result.returncode == 0:
652
- logger.info("āœ… kuzu-memory installed successfully")
653
- # Detect the newly installed path
654
- kuzu_memory_path = config_manager.detect_service_path("kuzu-memory")
655
- if not kuzu_memory_path:
656
- logger.warning(
657
- "kuzu-memory installed but command not found in PATH"
658
- )
659
- return
660
-
661
- # Update the Claude configuration to include the newly installed service
662
- logger.info("šŸ“ Updating Claude configuration...")
663
- config_success, config_msg = (
664
- config_manager.ensure_mcp_services_configured()
665
- )
666
- if config_success:
667
- logger.info(f"āœ… {config_msg}")
668
- else:
669
- logger.warning(f"āš ļø Configuration update issue: {config_msg}")
670
- else:
671
- logger.warning(f"Failed to install kuzu-memory: {result.stderr}")
672
- return
673
-
674
- except subprocess.TimeoutExpired:
675
- logger.warning("Installation of kuzu-memory timed out")
676
- return
677
- except Exception as e:
678
- logger.warning(f"Error installing kuzu-memory: {e}")
679
- return
680
-
681
- # At this point, kuzu-memory should be available
682
- # Get the actual command to use
683
- import shutil
684
-
685
- kuzu_memory_cmd = shutil.which("kuzu-memory")
686
- if not kuzu_memory_cmd:
687
- # Try pipx installation path as fallback
688
- pipx_path = Path.home() / ".local/pipx/venvs/kuzu-memory/bin/kuzu-memory"
689
- if pipx_path.exists():
690
- kuzu_memory_cmd = str(pipx_path)
691
- else:
692
- logger.debug("kuzu-memory command not found after installation")
693
- return
694
-
695
- # Check for kuzu-memory updates (non-blocking)
696
- try:
697
- await _check_kuzu_memory_updates(kuzu_memory_cmd)
698
- except Exception as e:
699
- logger.debug(f"Update check failed (non-critical): {e}")
700
-
701
- # Initialize kuzu-memory database in current project
702
- current_dir = Path.cwd()
703
- kuzu_memories_dir = current_dir / "kuzu-memories"
704
-
705
- # Check if database is already initialized
706
- if kuzu_memories_dir.exists():
707
- logger.debug(
708
- f"Kuzu-memory database already initialized at {kuzu_memories_dir}"
709
- )
710
-
711
- # Ensure kuzu-memories is in gitignore even if already initialized
712
- try:
713
- from ....services.project.project_organizer import ProjectOrganizer
714
-
715
- if (current_dir / ".claude-mpm").exists() or (
716
- current_dir / ".git"
717
- ).exists():
718
- organizer = ProjectOrganizer(current_dir)
719
- organizer.update_gitignore(additional_patterns=["kuzu-memories/"])
720
- logger.debug("Ensured kuzu-memories is in gitignore")
721
- except Exception as e:
722
- logger.debug(f"Could not update gitignore for kuzu-memories: {e}")
723
- else:
724
- logger.info(
725
- f"šŸŽÆ Initializing kuzu-memory database for project: {current_dir}"
726
- )
727
-
728
- # Initialize the database in current project directory
729
- import subprocess
730
-
731
- proc = subprocess.run(
732
- [kuzu_memory_cmd, "init"],
733
- capture_output=True,
734
- text=True,
735
- timeout=30,
736
- cwd=str(current_dir),
737
- check=False,
738
- )
739
-
740
- if proc.returncode == 0:
741
- logger.info("āœ… Kuzu-memory database initialized successfully")
742
-
743
- # Ensure kuzu-memories is in gitignore
744
- try:
745
- from ....services.project.project_organizer import ProjectOrganizer
746
-
747
- if (current_dir / ".claude-mpm").exists() or (
748
- current_dir / ".git"
749
- ).exists():
750
- organizer = ProjectOrganizer(current_dir)
751
- organizer.update_gitignore(
752
- additional_patterns=["kuzu-memories/"]
753
- )
754
- logger.debug("Ensured kuzu-memories is in gitignore")
755
- except Exception as e:
756
- logger.debug(f"Could not update gitignore for kuzu-memories: {e}")
757
- # Non-critical, don't fail initialization
758
- else:
759
- logger.warning(f"āš ļø Kuzu-memory initialization failed: {proc.stderr}")
760
-
761
- except Exception as e:
762
- logger.debug(f"Kuzu-memory auto-initialization error (non-critical): {e}")
763
-
764
-
765
- async def _check_kuzu_memory_updates(kuzu_cmd: Path) -> None:
766
- """
767
- Check for kuzu-memory updates and prompt user.
768
-
769
- Args:
770
- kuzu_cmd: Path to kuzu-memory command
771
-
772
- WHY: Keep users informed about important updates that may fix bugs
773
- or add features they need.
774
-
775
- DESIGN DECISIONS:
776
- - Non-blocking with timeout to prevent startup delays
777
- - Respects user preferences and environment variables
778
- - Only prompts in interactive TTY sessions
779
- """
780
- logger = get_logger("kuzu_memory_update")
781
-
782
- # Skip if environment variable set
783
- if os.environ.get("CLAUDE_MPM_SKIP_UPDATE_CHECK"):
784
- return
785
-
786
- # Skip if not TTY (can't prompt)
787
- if not sys.stdin.isatty():
788
- return
789
-
790
- # Import update utilities
791
- from ..utils.package_version_checker import PackageVersionChecker
792
- from ..utils.update_preferences import UpdatePreferences
793
-
794
- # Check if updates are enabled for this package
795
- if not UpdatePreferences.should_check_package("kuzu-memory"):
796
- return
797
-
798
- try:
799
- # Get current version from pipx
800
- result = subprocess.run(
801
- ["pipx", "list", "--json"],
802
- capture_output=True,
803
- text=True,
804
- timeout=5,
805
- check=False,
806
- )
807
-
808
- if result.returncode == 0:
809
- pipx_data = json.loads(result.stdout)
810
- venvs = pipx_data.get("venvs", {})
811
- kuzu_info = venvs.get("kuzu-memory", {})
812
- metadata = kuzu_info.get("metadata", {})
813
- current_version = metadata.get("main_package", {}).get(
814
- "package_version", "unknown"
815
- )
816
-
817
- if current_version != "unknown":
818
- # Check for updates
819
- checker = PackageVersionChecker()
820
- update_info = await checker.check_for_update(
821
- "kuzu-memory", current_version
822
- )
823
-
824
- if update_info and update_info.get("update_available"):
825
- latest_version = update_info["latest"]
826
-
827
- # Check if user wants to skip this version
828
- if UpdatePreferences.should_skip_version(
829
- "kuzu-memory", latest_version
830
- ):
831
- logger.debug(
832
- f"Skipping kuzu-memory update to {latest_version} per user preference"
833
- )
834
- return
835
-
836
- # Prompt for update
837
- _prompt_kuzu_update(update_info["current"], latest_version)
838
-
839
- except Exception as e:
840
- logger.debug(f"Update check error: {e}")
841
-
842
-
843
- def _prompt_kuzu_update(current: str, latest: str) -> None:
844
- """
845
- Prompt user to update kuzu-memory.
846
-
847
- Args:
848
- current: Current installed version
849
- latest: Latest available version
850
- """
851
- from ...cli.shared.error_handling import confirm_operation
852
- from ..utils.update_preferences import UpdatePreferences
853
-
854
- logger = get_logger("kuzu_memory_update")
855
-
856
- message = (
857
- f"\nšŸ”„ A new version of kuzu-memory is available!\n"
858
- f" Current: v{current}\n"
859
- f" Latest: v{latest}\n\n"
860
- f" This update may include bug fixes and performance improvements.\n"
861
- f" Update now?"
862
- )
863
-
864
- # Check if running in a non-interactive context
865
- try:
866
- if confirm_operation(message):
867
- print("šŸš€ Updating kuzu-memory...", file=sys.stderr)
868
- try:
869
- result = subprocess.run(
870
- ["pipx", "upgrade", "kuzu-memory"],
871
- capture_output=True,
872
- text=True,
873
- timeout=30,
874
- check=False,
875
- )
876
- if result.returncode == 0:
877
- print("āœ… Successfully updated kuzu-memory!", file=sys.stderr)
878
- logger.info(f"Updated kuzu-memory from {current} to {latest}")
879
- else:
880
- print(f"āš ļø Update failed: {result.stderr}", file=sys.stderr)
881
- logger.warning(f"kuzu-memory update failed: {result.stderr}")
882
- except subprocess.TimeoutExpired:
883
- print("āš ļø Update timed out. Please try again later.", file=sys.stderr)
884
- logger.warning("kuzu-memory update timed out")
885
- except Exception as e:
886
- print(f"āš ļø Update failed: {e}", file=sys.stderr)
887
- logger.warning(f"kuzu-memory update error: {e}")
888
- else:
889
- # User declined update
890
- print("\n To skip this version permanently, run:", file=sys.stderr)
891
- print(
892
- f" claude-mpm config set-skip-version kuzu-memory {latest}",
893
- file=sys.stderr,
894
- )
895
- print(" To disable update checks for kuzu-memory:", file=sys.stderr)
896
- print(
897
- " claude-mpm config disable-update-checks kuzu-memory",
898
- file=sys.stderr,
899
- )
900
-
901
- # Ask if user wants to skip this version
902
- if confirm_operation("\n Skip this version in future checks?"):
903
- UpdatePreferences.set_skip_version("kuzu-memory", latest)
904
- print(
905
- f" Version {latest} will be skipped in future checks.",
906
- file=sys.stderr,
907
- )
908
- except (KeyboardInterrupt, EOFError):
909
- # User interrupted or input not available
910
- pass
911
-
912
-
913
- async def pre_warm_mcp_servers():
914
- """
915
- Pre-warm MCP servers from configuration.
916
-
917
- DISABLED: This function is currently disabled to avoid conflicts with
918
- Claude Code's native MCP server management. When enabled, this can
919
- cause issues with MCP server initialization and stderr/stdout handling.
920
-
921
- TODO: Re-enable after ensuring compatibility with Claude Code's MCP handling.
922
- """
923
- logger = get_logger("MCPProcessPool")
924
- logger.debug("MCP server pre-warming is currently disabled")
925
-
926
- # COMMENTED OUT: Auto-initialization that can interfere with Claude Code
927
- # # Auto-initialize vector search for current project
928
- # await auto_initialize_vector_search()
929
- #
930
- # # Auto-initialize kuzu-memory for persistent knowledge
931
- # await auto_initialize_kuzu_memory()
932
- #
933
- # pool = get_process_pool()
934
- #
935
- # # Load MCP configurations
936
- # configs = {}
937
- #
938
- # # Check .claude.json for MCP server configs
939
- # claude_config_path = Path.home() / ".claude.json"
940
- # if not claude_config_path.exists():
941
- # # Try project-local config
942
- # claude_config_path = Path.cwd() / ".claude.json"
943
- #
944
- # if claude_config_path.exists():
945
- # try:
946
- # with claude_config_path.open() as f:
947
- # config_data = json.load(f)
948
- # mcp_servers = config_data.get("mcpServers", {})
949
- # configs.update(mcp_servers)
950
- # except Exception as e:
951
- # get_logger("MCPProcessPool").warning(f"Failed to load Claude config: {e}")
952
- #
953
- # # Check .mcp.json for additional configs
954
- # mcp_config_path = Path.cwd() / ".mcp.json"
955
- # if mcp_config_path.exists():
956
- # try:
957
- # with mcp_config_path.open() as f:
958
- # config_data = json.load(f)
959
- # mcp_servers = config_data.get("mcpServers", {})
960
- # configs.update(mcp_servers)
961
- # except Exception as e:
962
- # get_logger("MCPProcessPool").warning(f"Failed to load MCP config: {e}")
963
- #
964
- # if configs:
965
- # await pool.pre_warm_servers(configs)
966
- # await pool.start_health_monitoring()
967
- #
968
- # return pool
969
-
970
- # Return a basic pool instance without pre-warming
971
- return get_process_pool()