claude-mpm 4.0.32__py3-none-any.whl → 4.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +70 -2
- claude_mpm/agents/OUTPUT_STYLE.md +0 -11
- claude_mpm/agents/WORKFLOW.md +14 -2
- claude_mpm/agents/templates/documentation.json +51 -34
- claude_mpm/agents/templates/research.json +0 -11
- claude_mpm/cli/__init__.py +111 -33
- claude_mpm/cli/commands/agent_manager.py +10 -8
- claude_mpm/cli/commands/agents.py +82 -0
- claude_mpm/cli/commands/cleanup_orphaned_agents.py +150 -0
- claude_mpm/cli/commands/mcp_pipx_config.py +199 -0
- claude_mpm/cli/parsers/agents_parser.py +27 -0
- claude_mpm/cli/parsers/base_parser.py +6 -0
- claude_mpm/cli/startup_logging.py +75 -0
- claude_mpm/core/framework_loader.py +173 -84
- claude_mpm/dashboard/static/css/dashboard.css +449 -0
- claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
- claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
- claude_mpm/dashboard/static/js/components/build-tracker.js +323 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
- claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
- claude_mpm/dashboard/static/js/dashboard.js +207 -31
- claude_mpm/dashboard/static/js/socket-client.js +92 -11
- claude_mpm/dashboard/templates/index.html +1 -0
- claude_mpm/hooks/claude_hooks/connection_pool.py +25 -4
- claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
- claude_mpm/hooks/claude_hooks/hook_handler.py +125 -163
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +34 -48
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -1
- claude_mpm/services/agents/deployment/agent_template_builder.py +20 -11
- claude_mpm/services/agents/deployment/agent_version_manager.py +4 -1
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +396 -13
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
- claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -85
- claude_mpm/services/agents/memory/content_manager.py +98 -105
- claude_mpm/services/event_bus/__init__.py +18 -0
- claude_mpm/services/event_bus/config.py +165 -0
- claude_mpm/services/event_bus/event_bus.py +349 -0
- claude_mpm/services/event_bus/relay.py +297 -0
- claude_mpm/services/events/__init__.py +44 -0
- claude_mpm/services/events/consumers/__init__.py +18 -0
- claude_mpm/services/events/consumers/dead_letter.py +296 -0
- claude_mpm/services/events/consumers/logging.py +183 -0
- claude_mpm/services/events/consumers/metrics.py +242 -0
- claude_mpm/services/events/consumers/socketio.py +376 -0
- claude_mpm/services/events/core.py +470 -0
- claude_mpm/services/events/interfaces.py +230 -0
- claude_mpm/services/events/producers/__init__.py +14 -0
- claude_mpm/services/events/producers/hook.py +269 -0
- claude_mpm/services/events/producers/system.py +327 -0
- claude_mpm/services/mcp_gateway/auto_configure.py +372 -0
- claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
- claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
- claude_mpm/services/monitor_build_service.py +345 -0
- claude_mpm/services/socketio/event_normalizer.py +667 -0
- claude_mpm/services/socketio/handlers/connection.py +81 -23
- claude_mpm/services/socketio/handlers/hook.py +14 -5
- claude_mpm/services/socketio/migration_utils.py +329 -0
- claude_mpm/services/socketio/server/broadcaster.py +26 -33
- claude_mpm/services/socketio/server/core.py +29 -5
- claude_mpm/services/socketio/server/eventbus_integration.py +189 -0
- claude_mpm/services/socketio/server/main.py +25 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/METADATA +28 -9
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/RECORD +82 -56
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.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
|