claude-mpm 5.1.9__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 (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +46 -0
  3. claude_mpm/agents/agent_loader.py +10 -17
  4. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  5. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  6. claude_mpm/cli/commands/configure.py +1046 -149
  7. claude_mpm/cli/commands/configure_agent_display.py +13 -6
  8. claude_mpm/cli/commands/mpm_init/core.py +158 -1
  9. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  10. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  11. claude_mpm/cli/commands/summarize.py +413 -0
  12. claude_mpm/cli/executor.py +8 -0
  13. claude_mpm/cli/parsers/base_parser.py +5 -0
  14. claude_mpm/cli/startup.py +60 -53
  15. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  16. claude_mpm/config/agent_sources.py +27 -0
  17. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  18. claude_mpm/core/socketio_pool.py +3 -3
  19. claude_mpm/core/unified_agent_registry.py +5 -15
  20. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  21. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  22. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  23. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  24. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  25. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  26. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  27. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  28. claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
  30. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  31. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  32. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  33. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  34. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  35. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  36. claude_mpm/scripts/launch_monitor.py +93 -13
  37. claude_mpm/services/agents/agent_recommendation_service.py +279 -0
  38. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
  39. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +322 -53
  40. claude_mpm/services/agents/git_source_manager.py +20 -0
  41. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  42. claude_mpm/services/agents/toolchain_detector.py +6 -5
  43. claude_mpm/services/analysis/__init__.py +11 -1
  44. claude_mpm/services/analysis/clone_detector.py +1030 -0
  45. claude_mpm/services/command_deployment_service.py +0 -2
  46. claude_mpm/services/event_bus/config.py +3 -1
  47. claude_mpm/services/monitor/daemon.py +9 -2
  48. claude_mpm/services/monitor/daemon_manager.py +39 -3
  49. claude_mpm/services/monitor/server.py +225 -19
  50. claude_mpm/services/socketio/event_normalizer.py +15 -1
  51. claude_mpm/services/socketio/server/core.py +160 -21
  52. claude_mpm/services/version_control/git_operations.py +103 -0
  53. claude_mpm/utils/agent_filters.py +17 -44
  54. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +1 -77
  55. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +59 -114
  56. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
  57. claude_mpm/dashboard/analysis_runner.py +0 -455
  58. claude_mpm/dashboard/index.html +0 -13
  59. claude_mpm/dashboard/open_dashboard.py +0 -66
  60. claude_mpm/dashboard/static/css/activity.css +0 -1958
  61. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  62. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  63. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  64. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  65. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  66. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  67. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  68. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  69. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  70. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  71. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  72. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  73. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  74. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  75. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  76. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  77. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  78. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  79. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  80. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  81. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  82. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  83. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  84. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  85. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  86. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  87. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  88. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  89. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  90. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  91. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  92. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  93. claude_mpm/dashboard/templates/code_simple.html +0 -153
  94. claude_mpm/dashboard/templates/index.html +0 -606
  95. claude_mpm/dashboard/test_dashboard.html +0 -372
  96. claude_mpm/scripts/mcp_server.py +0 -75
  97. claude_mpm/scripts/mcp_wrapper.py +0 -39
  98. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  99. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  100. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  101. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  102. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  103. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  104. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  105. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  106. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  107. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  108. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  109. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  110. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  111. claude_mpm/services/mcp_gateway/main.py +0 -589
  112. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  113. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  114. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  115. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  116. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  117. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  118. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  119. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  120. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  121. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  122. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  123. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  124. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  125. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  126. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  127. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  128. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  129. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
  130. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
  131. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
@@ -1,977 +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
- # Verify the newly installed service is available
444
- logger.info("📝 Verifying installation...")
445
- available, msg = config_manager.check_mcp_services_available()
446
- if available:
447
- logger.info(f"✅ {msg}")
448
- else:
449
- logger.warning(
450
- f"⚠️ Service installed but not configured in Claude: {msg}"
451
- )
452
- logger.info(
453
- "💡 Configure via: Claude Desktop > Settings > Developer > Model Context Protocol"
454
- )
455
- else:
456
- logger.warning(
457
- f"Failed to install mcp-vector-search: {result.stderr}"
458
- )
459
- return
460
-
461
- except subprocess.TimeoutExpired:
462
- logger.warning("Installation of mcp-vector-search timed out")
463
- return
464
- except Exception as e:
465
- logger.warning(f"Error installing mcp-vector-search: {e}")
466
- return
467
-
468
- # At this point, mcp-vector-search should be available
469
- # Get the actual command to use
470
- import shutil
471
-
472
- vector_search_cmd = shutil.which("mcp-vector-search")
473
- if not vector_search_cmd:
474
- # Try pipx installation path as fallback
475
- pipx_path = (
476
- Path.home()
477
- / ".local/pipx/venvs/mcp-vector-search/bin/mcp-vector-search"
478
- )
479
- if pipx_path.exists():
480
- vector_search_cmd = str(pipx_path)
481
- else:
482
- logger.debug("mcp-vector-search command not found after installation")
483
- return
484
-
485
- # Check if current project is already initialized
486
- current_dir = Path.cwd()
487
- vector_config = current_dir / ".mcp-vector-search/config.json"
488
-
489
- if vector_config.exists():
490
- logger.debug(f"Vector search already initialized for {current_dir}")
491
-
492
- # Ensure .mcp-vector-search is in gitignore even if already initialized
493
- try:
494
- from ....services.project.project_organizer import ProjectOrganizer
495
-
496
- if (current_dir / ".claude-mpm").exists() or (
497
- current_dir / ".git"
498
- ).exists():
499
- organizer = ProjectOrganizer(current_dir)
500
- organizer.update_gitignore(
501
- additional_patterns=[".mcp-vector-search/"]
502
- )
503
- logger.debug("Ensured .mcp-vector-search is in gitignore")
504
- except Exception as e:
505
- logger.debug(f"Could not update gitignore for .mcp-vector-search: {e}")
506
- # Check if index needs rebuilding (corrupted database)
507
- chroma_db = current_dir / ".mcp-vector-search/chroma.sqlite3"
508
- if chroma_db.exists():
509
- # Quick health check - verify database file exists and is accessible
510
- try:
511
- # Check if database file exists and has reasonable size
512
- if chroma_db.exists() and chroma_db.stat().st_size > 0:
513
- logger.info("✓ Vector search index is healthy and ready")
514
- return
515
- logger.info("⚠️ Vector search index may be corrupted, rebuilding...")
516
- except Exception as e:
517
- logger.debug(
518
- f"Vector search health check failed: {e}, will attempt to rebuild"
519
- )
520
-
521
- # Initialize or reinitialize the project
522
- logger.info(f"🎯 Initializing vector search for project: {current_dir}")
523
-
524
- # Initialize the project (this creates the config)
525
- # Note: mcp-vector-search operates on the current directory
526
- import subprocess
527
-
528
- proc = subprocess.run(
529
- [vector_search_cmd, "init"],
530
- capture_output=True,
531
- text=True,
532
- timeout=30,
533
- cwd=str(current_dir),
534
- check=False, # Run in the project directory
535
- )
536
-
537
- if proc.returncode == 0:
538
- logger.info("✅ Vector search initialization completed")
539
-
540
- # Ensure .mcp-vector-search is in gitignore
541
- try:
542
- from ....services.project.project_organizer import ProjectOrganizer
543
-
544
- # Check if we're in a git repository (parent of .claude-mpm)
545
- if (current_dir / ".claude-mpm").exists() or (
546
- current_dir / ".git"
547
- ).exists():
548
- organizer = ProjectOrganizer(current_dir)
549
- organizer.update_gitignore(
550
- additional_patterns=[".mcp-vector-search/"]
551
- )
552
- logger.debug("Ensured .mcp-vector-search is in gitignore")
553
- except Exception as e:
554
- logger.debug(f"Could not update gitignore for .mcp-vector-search: {e}")
555
- # Non-critical, don't fail initialization
556
-
557
- # Start background indexing (non-blocking)
558
- def background_index():
559
- try:
560
- logger.info("🔄 Starting project indexing in background...")
561
- index_proc = subprocess.run(
562
- [vector_search_cmd, "index", "main"],
563
- capture_output=True,
564
- text=True,
565
- timeout=300, # 5 minute timeout for indexing
566
- cwd=str(current_dir),
567
- check=False, # Run in the project directory
568
- )
569
- if index_proc.returncode == 0:
570
- logger.info("✅ Project indexing completed successfully")
571
- # Parse output to show statistics if available
572
- if "indexed" in index_proc.stdout.lower():
573
- # Extract and log indexing statistics
574
- lines = index_proc.stdout.strip().split("\n")
575
- for line in lines:
576
- if "indexed" in line.lower() or "files" in line.lower():
577
- logger.info(f" {line.strip()}")
578
- else:
579
- logger.warning(
580
- f"⚠️ Project indexing failed: {index_proc.stderr}"
581
- )
582
- except subprocess.TimeoutExpired:
583
- logger.warning(
584
- "⚠️ Project indexing timed out (will continue in background)"
585
- )
586
- except Exception as e:
587
- logger.debug(f"Background indexing error (non-critical): {e}")
588
-
589
- # Run indexing in background thread
590
- import threading
591
-
592
- index_thread = threading.Thread(target=background_index, daemon=True)
593
- index_thread.start()
594
- logger.info(
595
- "📚 Background indexing started - vector search will be available shortly"
596
- )
597
-
598
- else:
599
- logger.warning(f"⚠️ Vector search initialization failed: {proc.stderr}")
600
-
601
- except Exception as e:
602
- logger.debug(f"Vector search auto-initialization error (non-critical): {e}")
603
-
604
-
605
- async def auto_initialize_kuzu_memory():
606
- """
607
- Auto-initialize kuzu-memory for persistent knowledge storage.
608
-
609
- WHY: Kuzu-memory provides a graph database for structured memory storage
610
- with semantic search capabilities, enabling persistent context across sessions.
611
-
612
- DESIGN DECISION:
613
- - Automatically install kuzu-memory if not present via pipx
614
- - Initialize database in background to avoid blocking startup
615
- - Failures are logged but don't prevent the system from starting
616
- """
617
- logger = get_logger("kuzu_memory_init")
618
-
619
- try:
620
- # Import MCPConfigManager to handle installation
621
- from claude_mpm.services.mcp_config_manager import MCPConfigManager
622
-
623
- config_manager = MCPConfigManager()
624
-
625
- # Check if kuzu-memory is already installed
626
- kuzu_memory_path = config_manager.detect_service_path("kuzu-memory")
627
-
628
- if kuzu_memory_path:
629
- logger.debug(f"kuzu-memory found at: {kuzu_memory_path}")
630
- else:
631
- # Not installed - attempt installation
632
- logger.info("🧠 kuzu-memory not found. Installing via pipx...")
633
-
634
- # First check if pipx is available
635
- import shutil
636
- import subprocess
637
-
638
- if not shutil.which("pipx"):
639
- logger.warning(
640
- "⚠️ pipx not found. Please install pipx to enable automatic kuzu-memory installation"
641
- )
642
- logger.info(" Install pipx with: python -m pip install --user pipx")
643
- return
644
-
645
- try:
646
- result = subprocess.run(
647
- ["pipx", "install", "kuzu-memory"],
648
- capture_output=True,
649
- text=True,
650
- timeout=60,
651
- check=False, # 1 minute timeout for installation
652
- )
653
-
654
- if result.returncode == 0:
655
- logger.info("✅ kuzu-memory installed successfully")
656
- # Detect the newly installed path
657
- kuzu_memory_path = config_manager.detect_service_path("kuzu-memory")
658
- if not kuzu_memory_path:
659
- logger.warning(
660
- "kuzu-memory installed but command not found in PATH"
661
- )
662
- return
663
-
664
- # Verify the newly installed service is available
665
- logger.info("📝 Verifying installation...")
666
- available, msg = config_manager.check_mcp_services_available()
667
- if available:
668
- logger.info(f"✅ {msg}")
669
- else:
670
- logger.warning(
671
- f"⚠️ Service installed but not configured in Claude: {msg}"
672
- )
673
- logger.info(
674
- "💡 Configure via: Claude Desktop > Settings > Developer > Model Context Protocol"
675
- )
676
- else:
677
- logger.warning(f"Failed to install kuzu-memory: {result.stderr}")
678
- return
679
-
680
- except subprocess.TimeoutExpired:
681
- logger.warning("Installation of kuzu-memory timed out")
682
- return
683
- except Exception as e:
684
- logger.warning(f"Error installing kuzu-memory: {e}")
685
- return
686
-
687
- # At this point, kuzu-memory should be available
688
- # Get the actual command to use
689
- import shutil
690
-
691
- kuzu_memory_cmd = shutil.which("kuzu-memory")
692
- if not kuzu_memory_cmd:
693
- # Try pipx installation path as fallback
694
- pipx_path = Path.home() / ".local/pipx/venvs/kuzu-memory/bin/kuzu-memory"
695
- if pipx_path.exists():
696
- kuzu_memory_cmd = str(pipx_path)
697
- else:
698
- logger.debug("kuzu-memory command not found after installation")
699
- return
700
-
701
- # Check for kuzu-memory updates (non-blocking)
702
- try:
703
- await _check_kuzu_memory_updates(kuzu_memory_cmd)
704
- except Exception as e:
705
- logger.debug(f"Update check failed (non-critical): {e}")
706
-
707
- # Initialize kuzu-memory database in current project
708
- current_dir = Path.cwd()
709
- kuzu_memories_dir = current_dir / "kuzu-memories"
710
-
711
- # Check if database is already initialized
712
- if kuzu_memories_dir.exists():
713
- logger.debug(
714
- f"Kuzu-memory database already initialized at {kuzu_memories_dir}"
715
- )
716
-
717
- # Ensure kuzu-memories is in gitignore even if already initialized
718
- try:
719
- from ....services.project.project_organizer import ProjectOrganizer
720
-
721
- if (current_dir / ".claude-mpm").exists() or (
722
- current_dir / ".git"
723
- ).exists():
724
- organizer = ProjectOrganizer(current_dir)
725
- organizer.update_gitignore(additional_patterns=["kuzu-memories/"])
726
- logger.debug("Ensured kuzu-memories is in gitignore")
727
- except Exception as e:
728
- logger.debug(f"Could not update gitignore for kuzu-memories: {e}")
729
- else:
730
- logger.info(
731
- f"🎯 Initializing kuzu-memory database for project: {current_dir}"
732
- )
733
-
734
- # Initialize the database in current project directory
735
- import subprocess
736
-
737
- proc = subprocess.run(
738
- [kuzu_memory_cmd, "init"],
739
- capture_output=True,
740
- text=True,
741
- timeout=30,
742
- cwd=str(current_dir),
743
- check=False,
744
- )
745
-
746
- if proc.returncode == 0:
747
- logger.info("✅ Kuzu-memory database initialized successfully")
748
-
749
- # Ensure kuzu-memories is in gitignore
750
- try:
751
- from ....services.project.project_organizer import ProjectOrganizer
752
-
753
- if (current_dir / ".claude-mpm").exists() or (
754
- current_dir / ".git"
755
- ).exists():
756
- organizer = ProjectOrganizer(current_dir)
757
- organizer.update_gitignore(
758
- additional_patterns=["kuzu-memories/"]
759
- )
760
- logger.debug("Ensured kuzu-memories is in gitignore")
761
- except Exception as e:
762
- logger.debug(f"Could not update gitignore for kuzu-memories: {e}")
763
- # Non-critical, don't fail initialization
764
- else:
765
- logger.warning(f"⚠️ Kuzu-memory initialization failed: {proc.stderr}")
766
-
767
- except Exception as e:
768
- logger.debug(f"Kuzu-memory auto-initialization error (non-critical): {e}")
769
-
770
-
771
- async def _check_kuzu_memory_updates(kuzu_cmd: Path) -> None:
772
- """
773
- Check for kuzu-memory updates and prompt user.
774
-
775
- Args:
776
- kuzu_cmd: Path to kuzu-memory command
777
-
778
- WHY: Keep users informed about important updates that may fix bugs
779
- or add features they need.
780
-
781
- DESIGN DECISIONS:
782
- - Non-blocking with timeout to prevent startup delays
783
- - Respects user preferences and environment variables
784
- - Only prompts in interactive TTY sessions
785
- """
786
- logger = get_logger("kuzu_memory_update")
787
-
788
- # Skip if environment variable set
789
- if os.environ.get("CLAUDE_MPM_SKIP_UPDATE_CHECK"):
790
- return
791
-
792
- # Skip if not TTY (can't prompt)
793
- if not sys.stdin.isatty():
794
- return
795
-
796
- # Import update utilities
797
- from ..utils.package_version_checker import PackageVersionChecker
798
- from ..utils.update_preferences import UpdatePreferences
799
-
800
- # Check if updates are enabled for this package
801
- if not UpdatePreferences.should_check_package("kuzu-memory"):
802
- return
803
-
804
- try:
805
- # Get current version from pipx
806
- result = subprocess.run(
807
- ["pipx", "list", "--json"],
808
- capture_output=True,
809
- text=True,
810
- timeout=5,
811
- check=False,
812
- )
813
-
814
- if result.returncode == 0:
815
- pipx_data = json.loads(result.stdout)
816
- venvs = pipx_data.get("venvs", {})
817
- kuzu_info = venvs.get("kuzu-memory", {})
818
- metadata = kuzu_info.get("metadata", {})
819
- current_version = metadata.get("main_package", {}).get(
820
- "package_version", "unknown"
821
- )
822
-
823
- if current_version != "unknown":
824
- # Check for updates
825
- checker = PackageVersionChecker()
826
- update_info = await checker.check_for_update(
827
- "kuzu-memory", current_version
828
- )
829
-
830
- if update_info and update_info.get("update_available"):
831
- latest_version = update_info["latest"]
832
-
833
- # Check if user wants to skip this version
834
- if UpdatePreferences.should_skip_version(
835
- "kuzu-memory", latest_version
836
- ):
837
- logger.debug(
838
- f"Skipping kuzu-memory update to {latest_version} per user preference"
839
- )
840
- return
841
-
842
- # Prompt for update
843
- _prompt_kuzu_update(update_info["current"], latest_version)
844
-
845
- except Exception as e:
846
- logger.debug(f"Update check error: {e}")
847
-
848
-
849
- def _prompt_kuzu_update(current: str, latest: str) -> None:
850
- """
851
- Prompt user to update kuzu-memory.
852
-
853
- Args:
854
- current: Current installed version
855
- latest: Latest available version
856
- """
857
- from ...cli.shared.error_handling import confirm_operation
858
- from ..utils.update_preferences import UpdatePreferences
859
-
860
- logger = get_logger("kuzu_memory_update")
861
-
862
- message = (
863
- f"\n🔄 A new version of kuzu-memory is available!\n"
864
- f" Current: v{current}\n"
865
- f" Latest: v{latest}\n\n"
866
- f" This update may include bug fixes and performance improvements.\n"
867
- f" Update now?"
868
- )
869
-
870
- # Check if running in a non-interactive context
871
- try:
872
- if confirm_operation(message):
873
- print("🚀 Updating kuzu-memory...", file=sys.stderr)
874
- try:
875
- result = subprocess.run(
876
- ["pipx", "upgrade", "kuzu-memory"],
877
- capture_output=True,
878
- text=True,
879
- timeout=30,
880
- check=False,
881
- )
882
- if result.returncode == 0:
883
- print("✅ Successfully updated kuzu-memory!", file=sys.stderr)
884
- logger.info(f"Updated kuzu-memory from {current} to {latest}")
885
- else:
886
- print(f"⚠️ Update failed: {result.stderr}", file=sys.stderr)
887
- logger.warning(f"kuzu-memory update failed: {result.stderr}")
888
- except subprocess.TimeoutExpired:
889
- print("⚠️ Update timed out. Please try again later.", file=sys.stderr)
890
- logger.warning("kuzu-memory update timed out")
891
- except Exception as e:
892
- print(f"⚠️ Update failed: {e}", file=sys.stderr)
893
- logger.warning(f"kuzu-memory update error: {e}")
894
- else:
895
- # User declined update
896
- print("\n To skip this version permanently, run:", file=sys.stderr)
897
- print(
898
- f" claude-mpm config set-skip-version kuzu-memory {latest}",
899
- file=sys.stderr,
900
- )
901
- print(" To disable update checks for kuzu-memory:", file=sys.stderr)
902
- print(
903
- " claude-mpm config disable-update-checks kuzu-memory",
904
- file=sys.stderr,
905
- )
906
-
907
- # Ask if user wants to skip this version
908
- if confirm_operation("\n Skip this version in future checks?"):
909
- UpdatePreferences.set_skip_version("kuzu-memory", latest)
910
- print(
911
- f" Version {latest} will be skipped in future checks.",
912
- file=sys.stderr,
913
- )
914
- except (KeyboardInterrupt, EOFError):
915
- # User interrupted or input not available
916
- pass
917
-
918
-
919
- async def pre_warm_mcp_servers():
920
- """
921
- Pre-warm MCP servers from configuration.
922
-
923
- DISABLED: This function is currently disabled to avoid conflicts with
924
- Claude Code's native MCP server management. When enabled, this can
925
- cause issues with MCP server initialization and stderr/stdout handling.
926
-
927
- TODO: Re-enable after ensuring compatibility with Claude Code's MCP handling.
928
- """
929
- logger = get_logger("MCPProcessPool")
930
- logger.debug("MCP server pre-warming is currently disabled")
931
-
932
- # COMMENTED OUT: Auto-initialization that can interfere with Claude Code
933
- # # Auto-initialize vector search for current project
934
- # await auto_initialize_vector_search()
935
- #
936
- # # Auto-initialize kuzu-memory for persistent knowledge
937
- # await auto_initialize_kuzu_memory()
938
- #
939
- # pool = get_process_pool()
940
- #
941
- # # Load MCP configurations
942
- # configs = {}
943
- #
944
- # # Check .claude.json for MCP server configs
945
- # claude_config_path = Path.home() / ".claude.json"
946
- # if not claude_config_path.exists():
947
- # # Try project-local config
948
- # claude_config_path = Path.cwd() / ".claude.json"
949
- #
950
- # if claude_config_path.exists():
951
- # try:
952
- # with claude_config_path.open() as f:
953
- # config_data = json.load(f)
954
- # mcp_servers = config_data.get("mcpServers", {})
955
- # configs.update(mcp_servers)
956
- # except Exception as e:
957
- # get_logger("MCPProcessPool").warning(f"Failed to load Claude config: {e}")
958
- #
959
- # # Check .mcp.json for additional configs
960
- # mcp_config_path = Path.cwd() / ".mcp.json"
961
- # if mcp_config_path.exists():
962
- # try:
963
- # with mcp_config_path.open() as f:
964
- # config_data = json.load(f)
965
- # mcp_servers = config_data.get("mcpServers", {})
966
- # configs.update(mcp_servers)
967
- # except Exception as e:
968
- # get_logger("MCPProcessPool").warning(f"Failed to load MCP config: {e}")
969
- #
970
- # if configs:
971
- # await pool.pre_warm_servers(configs)
972
- # await pool.start_health_monitoring()
973
- #
974
- # return pool
975
-
976
- # Return a basic pool instance without pre-warming
977
- return get_process_pool()