claude-mpm 4.0.32__py3-none-any.whl → 4.0.34__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 (67) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/documentation.json +51 -34
  3. claude_mpm/agents/templates/research.json +0 -11
  4. claude_mpm/cli/__init__.py +63 -26
  5. claude_mpm/cli/commands/agent_manager.py +10 -8
  6. claude_mpm/core/framework_loader.py +173 -84
  7. claude_mpm/dashboard/static/css/dashboard.css +449 -0
  8. claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
  9. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  10. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
  11. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  12. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  13. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  14. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  15. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
  16. claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
  17. claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
  18. claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
  19. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
  20. claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
  21. claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
  22. claude_mpm/dashboard/static/js/dashboard.js +207 -31
  23. claude_mpm/dashboard/static/js/socket-client.js +85 -6
  24. claude_mpm/dashboard/templates/index.html +1 -0
  25. claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
  26. claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
  28. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
  29. claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
  30. claude_mpm/services/agents/deployment/agent_deployment.py +34 -48
  31. claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
  32. claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
  33. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
  34. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
  35. claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
  36. claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
  37. claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -85
  38. claude_mpm/services/agents/memory/content_manager.py +98 -105
  39. claude_mpm/services/event_bus/__init__.py +18 -0
  40. claude_mpm/services/event_bus/event_bus.py +334 -0
  41. claude_mpm/services/event_bus/relay.py +301 -0
  42. claude_mpm/services/events/__init__.py +44 -0
  43. claude_mpm/services/events/consumers/__init__.py +18 -0
  44. claude_mpm/services/events/consumers/dead_letter.py +296 -0
  45. claude_mpm/services/events/consumers/logging.py +183 -0
  46. claude_mpm/services/events/consumers/metrics.py +242 -0
  47. claude_mpm/services/events/consumers/socketio.py +376 -0
  48. claude_mpm/services/events/core.py +470 -0
  49. claude_mpm/services/events/interfaces.py +230 -0
  50. claude_mpm/services/events/producers/__init__.py +14 -0
  51. claude_mpm/services/events/producers/hook.py +269 -0
  52. claude_mpm/services/events/producers/system.py +327 -0
  53. claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
  54. claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
  55. claude_mpm/services/monitor_build_service.py +345 -0
  56. claude_mpm/services/socketio/event_normalizer.py +667 -0
  57. claude_mpm/services/socketio/handlers/connection.py +78 -20
  58. claude_mpm/services/socketio/handlers/hook.py +14 -5
  59. claude_mpm/services/socketio/migration_utils.py +329 -0
  60. claude_mpm/services/socketio/server/broadcaster.py +26 -33
  61. claude_mpm/services/socketio/server/core.py +4 -3
  62. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
  63. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +67 -46
  64. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
  65. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
  66. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
  67. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,411 @@
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, List, Optional, Tuple
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
+ def cleanup_handler(signum, frame):
88
+ self.logger.info(f"Received signal {signum}, cleaning up process pool")
89
+ self.cleanup_all()
90
+
91
+ signal.signal(signal.SIGTERM, cleanup_handler)
92
+ signal.signal(signal.SIGINT, cleanup_handler)
93
+
94
+ def get_or_create_process(self, server_name: str, config: Dict) -> Optional[subprocess.Popen]:
95
+ """
96
+ Get an existing process or create a new one for the given server.
97
+
98
+ Args:
99
+ server_name: Name of the MCP server
100
+ config: Server configuration including command and args
101
+
102
+ Returns:
103
+ Process handle or None if failed
104
+ """
105
+ start_time = time.time()
106
+
107
+ # Check if we have a healthy existing process
108
+ if server_name in self._processes:
109
+ process = self._processes[server_name]
110
+ if self._is_process_healthy(process):
111
+ self.logger.info(f"Reusing existing process for {server_name} (PID: {process.pid})")
112
+ return process
113
+ else:
114
+ # Process is dead, clean it up
115
+ self.logger.warning(f"Process for {server_name} is dead, cleaning up")
116
+ self._cleanup_process(server_name)
117
+
118
+ # Check if we've hit the process limit
119
+ if len(self._processes) >= self.max_processes:
120
+ # Find and clean up the oldest idle process
121
+ self._cleanup_oldest_idle_process()
122
+
123
+ # Create new process
124
+ self.logger.info(f"Creating new process for {server_name}")
125
+ process = self._create_process(server_name, config)
126
+
127
+ if process:
128
+ create_time = time.time() - start_time
129
+ self.logger.info(f"Process created for {server_name} in {create_time:.2f}s (PID: {process.pid})")
130
+ self._startup_times[server_name] = create_time
131
+
132
+ return process
133
+
134
+ def _create_process(self, server_name: str, config: Dict) -> Optional[subprocess.Popen]:
135
+ """
136
+ Create a new MCP server process.
137
+
138
+ Args:
139
+ server_name: Name of the MCP server
140
+ config: Server configuration
141
+
142
+ Returns:
143
+ Process handle or None if failed
144
+ """
145
+ try:
146
+ # Extract command and args from config
147
+ command = config.get("command", "")
148
+ args = config.get("args", [])
149
+ env = config.get("env", {})
150
+ cwd = config.get("cwd")
151
+
152
+ # Build full command
153
+ full_command = [command] + args
154
+
155
+ # Merge environment variables
156
+ process_env = os.environ.copy()
157
+ process_env.update(env)
158
+
159
+ # Add timing instrumentation
160
+ process_env["MCP_STARTUP_TRACKING"] = "1"
161
+ process_env["MCP_SERVER_NAME"] = server_name
162
+
163
+ # Start the process
164
+ process = subprocess.Popen(
165
+ full_command,
166
+ stdin=subprocess.PIPE,
167
+ stdout=subprocess.PIPE,
168
+ stderr=subprocess.PIPE,
169
+ env=process_env,
170
+ cwd=cwd,
171
+ bufsize=0 # Unbuffered for real-time communication
172
+ )
173
+
174
+ # Store process info
175
+ self._processes[server_name] = process
176
+ self._process_info[server_name] = {
177
+ "pid": process.pid,
178
+ "started_at": time.time(),
179
+ "last_used": time.time(),
180
+ "config": config
181
+ }
182
+
183
+ # Write process info to file for debugging
184
+ info_file = self.pool_dir / f"{server_name}_{process.pid}.json"
185
+ with open(info_file, 'w') as f:
186
+ json.dump(self._process_info[server_name], f, indent=2)
187
+
188
+ return process
189
+
190
+ except Exception as e:
191
+ self.logger.error(f"Failed to create process for {server_name}: {e}")
192
+ return None
193
+
194
+ def _is_process_healthy(self, process: subprocess.Popen) -> bool:
195
+ """Check if a process is still running and healthy."""
196
+ if process.poll() is not None:
197
+ # Process has terminated
198
+ return False
199
+
200
+ try:
201
+ # Send signal 0 to check if process is alive
202
+ os.kill(process.pid, 0)
203
+ return True
204
+ except (OSError, ProcessLookupError):
205
+ return False
206
+
207
+ def _cleanup_process(self, server_name: str):
208
+ """Clean up a specific process."""
209
+ if server_name not in self._processes:
210
+ return
211
+
212
+ process = self._processes[server_name]
213
+
214
+ try:
215
+ # Try graceful shutdown first
216
+ if self._is_process_healthy(process):
217
+ process.terminate()
218
+ try:
219
+ process.wait(timeout=5)
220
+ except subprocess.TimeoutExpired:
221
+ # Force kill if graceful shutdown fails
222
+ process.kill()
223
+ process.wait()
224
+
225
+ # Remove from tracking
226
+ del self._processes[server_name]
227
+ del self._process_info[server_name]
228
+
229
+ # Clean up info file
230
+ for info_file in self.pool_dir.glob(f"{server_name}_*.json"):
231
+ info_file.unlink()
232
+
233
+ self.logger.info(f"Cleaned up process for {server_name}")
234
+
235
+ except Exception as e:
236
+ self.logger.warning(f"Error cleaning up process for {server_name}: {e}")
237
+
238
+ def _cleanup_oldest_idle_process(self):
239
+ """Find and clean up the oldest idle process."""
240
+ if not self._process_info:
241
+ return
242
+
243
+ # Find process with oldest last_used time
244
+ oldest_server = min(
245
+ self._process_info.keys(),
246
+ key=lambda k: self._process_info[k].get("last_used", 0)
247
+ )
248
+
249
+ self.logger.info(f"Cleaning up oldest idle process: {oldest_server}")
250
+ self._cleanup_process(oldest_server)
251
+
252
+ async def pre_warm_servers(self, configs: Dict[str, Dict]):
253
+ """
254
+ Pre-warm MCP servers during framework initialization.
255
+
256
+ Args:
257
+ configs: Dictionary of server configurations
258
+ """
259
+ if self._pre_warmed:
260
+ self.logger.info("Servers already pre-warmed")
261
+ return
262
+
263
+ self.logger.info(f"Pre-warming {len(configs)} MCP servers")
264
+ start_time = time.time()
265
+
266
+ # Start all servers in parallel
267
+ tasks = []
268
+ for server_name, config in configs.items():
269
+ # Only pre-warm critical servers (like vector search)
270
+ if "vector" in server_name.lower() or config.get("pre_warm", False):
271
+ self.logger.info(f"Pre-warming {server_name}")
272
+ process = self.get_or_create_process(server_name, config)
273
+ if process:
274
+ self.logger.info(f"Pre-warmed {server_name} (PID: {process.pid})")
275
+
276
+ self._pre_warmed = True
277
+ total_time = time.time() - start_time
278
+ self.logger.info(f"Pre-warming completed in {total_time:.2f}s")
279
+
280
+ async def start_health_monitoring(self):
281
+ """Start background health monitoring of processes."""
282
+ if self._health_check_task and not self._health_check_task.done():
283
+ return
284
+
285
+ self._health_check_task = asyncio.create_task(self._health_check_loop())
286
+ self.logger.info("Started health monitoring")
287
+
288
+ async def _health_check_loop(self):
289
+ """Background loop to check process health."""
290
+ while True:
291
+ try:
292
+ await asyncio.sleep(self.health_check_interval)
293
+
294
+ # Check each process
295
+ dead_processes = []
296
+ for server_name, process in self._processes.items():
297
+ if not self._is_process_healthy(process):
298
+ dead_processes.append(server_name)
299
+
300
+ # Clean up dead processes
301
+ for server_name in dead_processes:
302
+ self.logger.warning(f"Process {server_name} is dead, cleaning up")
303
+ self._cleanup_process(server_name)
304
+
305
+ # Check for idle timeout
306
+ current_time = time.time()
307
+ idle_processes = []
308
+ for server_name, info in self._process_info.items():
309
+ last_used = info.get("last_used", current_time)
310
+ if current_time - last_used > self.process_timeout:
311
+ idle_processes.append(server_name)
312
+
313
+ # Clean up idle processes
314
+ for server_name in idle_processes:
315
+ self.logger.info(f"Process {server_name} idle timeout, cleaning up")
316
+ self._cleanup_process(server_name)
317
+
318
+ except Exception as e:
319
+ self.logger.error(f"Error in health check loop: {e}")
320
+
321
+ def mark_process_used(self, server_name: str):
322
+ """Mark a process as recently used."""
323
+ if server_name in self._process_info:
324
+ self._process_info[server_name]["last_used"] = time.time()
325
+
326
+ def get_startup_metrics(self) -> Dict[str, float]:
327
+ """Get startup time metrics for all servers."""
328
+ return self._startup_times.copy()
329
+
330
+ def get_pool_status(self) -> Dict[str, Any]:
331
+ """Get current status of the process pool."""
332
+ return {
333
+ "active_processes": len(self._processes),
334
+ "max_processes": self.max_processes,
335
+ "pre_warmed": self._pre_warmed,
336
+ "processes": {
337
+ name: {
338
+ "pid": info.get("pid"),
339
+ "uptime": time.time() - info.get("started_at", time.time()),
340
+ "idle_time": time.time() - info.get("last_used", time.time())
341
+ }
342
+ for name, info in self._process_info.items()
343
+ },
344
+ "startup_metrics": self._startup_times
345
+ }
346
+
347
+ def cleanup_all(self):
348
+ """Clean up all processes in the pool."""
349
+ self.logger.info("Cleaning up all processes in pool")
350
+
351
+ # Stop health monitoring
352
+ if self._health_check_task:
353
+ self._health_check_task.cancel()
354
+
355
+ # Clean up all processes
356
+ for server_name in list(self._processes.keys()):
357
+ self._cleanup_process(server_name)
358
+
359
+ self.logger.info("Process pool cleanup completed")
360
+
361
+
362
+ # Global instance
363
+ _pool: Optional[MCPProcessPool] = None
364
+
365
+
366
+ def get_process_pool() -> MCPProcessPool:
367
+ """Get the global MCP process pool instance."""
368
+ global _pool
369
+ if _pool is None:
370
+ _pool = MCPProcessPool()
371
+ return _pool
372
+
373
+
374
+ async def pre_warm_mcp_servers():
375
+ """Pre-warm MCP servers from configuration."""
376
+ pool = get_process_pool()
377
+
378
+ # Load MCP configurations
379
+ configs = {}
380
+
381
+ # Check .claude.json for MCP server configs
382
+ claude_config_path = Path.home() / ".claude.json"
383
+ if not claude_config_path.exists():
384
+ # Try project-local config
385
+ claude_config_path = Path.cwd() / ".claude.json"
386
+
387
+ if claude_config_path.exists():
388
+ try:
389
+ with open(claude_config_path, 'r') as f:
390
+ config_data = json.load(f)
391
+ mcp_servers = config_data.get("mcpServers", {})
392
+ configs.update(mcp_servers)
393
+ except Exception as e:
394
+ get_logger("MCPProcessPool").warning(f"Failed to load Claude config: {e}")
395
+
396
+ # Check .mcp.json for additional configs
397
+ mcp_config_path = Path.cwd() / ".mcp.json"
398
+ if mcp_config_path.exists():
399
+ try:
400
+ with open(mcp_config_path, 'r') as f:
401
+ config_data = json.load(f)
402
+ mcp_servers = config_data.get("mcpServers", {})
403
+ configs.update(mcp_servers)
404
+ except Exception as e:
405
+ get_logger("MCPProcessPool").warning(f"Failed to load MCP config: {e}")
406
+
407
+ if configs:
408
+ await pool.pre_warm_servers(configs)
409
+ await pool.start_health_monitoring()
410
+
411
+ return pool
@@ -15,7 +15,10 @@ use JSON-RPC protocol, and exit cleanly when stdin closes.
15
15
  import asyncio
16
16
  import json
17
17
  import logging
18
+ import os
18
19
  import sys
20
+ import time
21
+ from pathlib import Path
19
22
  from typing import Any, Dict, Optional
20
23
 
21
24
  # Import MCP SDK components
@@ -28,6 +31,7 @@ from mcp.types import TextContent, Tool
28
31
  from pydantic import BaseModel
29
32
 
30
33
  from claude_mpm.core.logger import get_logger
34
+ from claude_mpm.services.mcp_gateway.core.singleton_manager import get_gateway_manager
31
35
 
32
36
  # Import unified ticket tool if available
33
37
  try:
@@ -135,6 +139,11 @@ class SimpleMCPServer:
135
139
  self.name = name
136
140
  self.version = version
137
141
  self.logger = get_logger("MCPStdioServer")
142
+ self.startup_time = time.time()
143
+
144
+ # Log startup timing
145
+ self.logger.info(f"Initializing MCP server {name} v{version}")
146
+ start_time = time.time()
138
147
 
139
148
  # Apply backward compatibility patches before creating server
140
149
  apply_backward_compatibility_patches()
@@ -144,6 +153,10 @@ class SimpleMCPServer:
144
153
 
145
154
  # Register default tools
146
155
  self._register_tools()
156
+
157
+ # Log initialization time
158
+ init_time = time.time() - start_time
159
+ self.logger.info(f"MCP server initialized in {init_time:.2f} seconds")
147
160
 
148
161
  async def _summarize_content(
149
162
  self, content: str, style: str, max_length: int