claude-mpm 4.0.34__py3-none-any.whl → 4.1.1__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 (35) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +70 -2
  3. claude_mpm/agents/OUTPUT_STYLE.md +0 -11
  4. claude_mpm/agents/WORKFLOW.md +14 -2
  5. claude_mpm/agents/templates/web_qa.json +85 -58
  6. claude_mpm/agents/templates/web_ui.json +3 -3
  7. claude_mpm/cli/__init__.py +48 -7
  8. claude_mpm/cli/commands/agents.py +82 -0
  9. claude_mpm/cli/commands/cleanup_orphaned_agents.py +150 -0
  10. claude_mpm/cli/commands/mcp_pipx_config.py +199 -0
  11. claude_mpm/cli/parsers/agents_parser.py +27 -0
  12. claude_mpm/cli/parsers/base_parser.py +6 -0
  13. claude_mpm/cli/startup_logging.py +75 -0
  14. claude_mpm/dashboard/static/js/components/build-tracker.js +35 -1
  15. claude_mpm/dashboard/static/js/socket-client.js +7 -5
  16. claude_mpm/hooks/claude_hooks/connection_pool.py +13 -2
  17. claude_mpm/hooks/claude_hooks/hook_handler.py +67 -167
  18. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -1
  19. claude_mpm/services/agents/deployment/agent_template_builder.py +2 -1
  20. claude_mpm/services/agents/deployment/agent_version_manager.py +4 -1
  21. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +207 -10
  22. claude_mpm/services/event_bus/config.py +165 -0
  23. claude_mpm/services/event_bus/event_bus.py +35 -20
  24. claude_mpm/services/event_bus/relay.py +8 -12
  25. claude_mpm/services/mcp_gateway/auto_configure.py +372 -0
  26. claude_mpm/services/socketio/handlers/connection.py +3 -3
  27. claude_mpm/services/socketio/server/core.py +25 -2
  28. claude_mpm/services/socketio/server/eventbus_integration.py +189 -0
  29. claude_mpm/services/socketio/server/main.py +25 -0
  30. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/METADATA +25 -7
  31. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/RECORD +35 -30
  32. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/WHEEL +0 -0
  33. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/entry_points.txt +0 -0
  34. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/licenses/LICENSE +0 -0
  35. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,165 @@
1
+ """Configuration for EventBus service.
2
+
3
+ WHY configuration module:
4
+ - Centralized configuration management
5
+ - Environment variable support
6
+ - Easy testing with different configurations
7
+ - Runtime configuration changes
8
+ """
9
+
10
+ import os
11
+ from typing import List, Optional
12
+ from dataclasses import dataclass, field
13
+
14
+
15
+ @dataclass
16
+ class EventBusConfig:
17
+ """Configuration for EventBus service.
18
+
19
+ All settings can be overridden via environment variables.
20
+ """
21
+
22
+ # Enable/disable the EventBus
23
+ enabled: bool = field(
24
+ default_factory=lambda: os.environ.get("CLAUDE_MPM_EVENTBUS_ENABLED", "true").lower() == "true"
25
+ )
26
+
27
+ # Debug logging
28
+ debug: bool = field(
29
+ default_factory=lambda: os.environ.get("CLAUDE_MPM_EVENTBUS_DEBUG", "false").lower() == "true"
30
+ )
31
+
32
+ # Event history settings
33
+ max_history_size: int = field(
34
+ default_factory=lambda: int(os.environ.get("CLAUDE_MPM_EVENTBUS_HISTORY_SIZE", "100"))
35
+ )
36
+
37
+ # Event filters (comma-separated list)
38
+ event_filters: List[str] = field(
39
+ default_factory=lambda: [
40
+ f.strip() for f in os.environ.get("CLAUDE_MPM_EVENTBUS_FILTERS", "").split(",")
41
+ if f.strip()
42
+ ]
43
+ )
44
+
45
+ # Relay configuration
46
+ relay_enabled: bool = field(
47
+ default_factory=lambda: os.environ.get("CLAUDE_MPM_RELAY_ENABLED", "true").lower() == "true"
48
+ )
49
+
50
+ relay_port: int = field(
51
+ default_factory=lambda: int(os.environ.get("CLAUDE_MPM_SOCKETIO_PORT", "8765"))
52
+ )
53
+
54
+ relay_debug: bool = field(
55
+ default_factory=lambda: os.environ.get("CLAUDE_MPM_RELAY_DEBUG", "false").lower() == "true"
56
+ )
57
+
58
+ # Connection settings
59
+ relay_max_retries: int = field(
60
+ default_factory=lambda: int(os.environ.get("CLAUDE_MPM_RELAY_MAX_RETRIES", "3"))
61
+ )
62
+
63
+ relay_retry_delay: float = field(
64
+ default_factory=lambda: float(os.environ.get("CLAUDE_MPM_RELAY_RETRY_DELAY", "0.5"))
65
+ )
66
+
67
+ relay_connection_cooldown: float = field(
68
+ default_factory=lambda: float(os.environ.get("CLAUDE_MPM_RELAY_CONNECTION_COOLDOWN", "5.0"))
69
+ )
70
+
71
+ @classmethod
72
+ def from_env(cls) -> "EventBusConfig":
73
+ """Create configuration from environment variables.
74
+
75
+ Returns:
76
+ EventBusConfig: Configuration instance
77
+ """
78
+ return cls()
79
+
80
+ def to_dict(self) -> dict:
81
+ """Convert configuration to dictionary.
82
+
83
+ Returns:
84
+ dict: Configuration as dictionary
85
+ """
86
+ return {
87
+ "enabled": self.enabled,
88
+ "debug": self.debug,
89
+ "max_history_size": self.max_history_size,
90
+ "event_filters": self.event_filters,
91
+ "relay_enabled": self.relay_enabled,
92
+ "relay_port": self.relay_port,
93
+ "relay_debug": self.relay_debug,
94
+ "relay_max_retries": self.relay_max_retries,
95
+ "relay_retry_delay": self.relay_retry_delay,
96
+ "relay_connection_cooldown": self.relay_connection_cooldown
97
+ }
98
+
99
+ def apply_to_eventbus(self, event_bus) -> None:
100
+ """Apply configuration to an EventBus instance.
101
+
102
+ Args:
103
+ event_bus: EventBus instance to configure
104
+ """
105
+ if not self.enabled:
106
+ event_bus.disable()
107
+ else:
108
+ event_bus.enable()
109
+
110
+ event_bus.set_debug(self.debug)
111
+ event_bus._max_history_size = self.max_history_size
112
+
113
+ # Apply filters
114
+ event_bus.clear_filters()
115
+ for filter_pattern in self.event_filters:
116
+ event_bus.add_filter(filter_pattern)
117
+
118
+ def apply_to_relay(self, relay) -> None:
119
+ """Apply configuration to a SocketIORelay instance.
120
+
121
+ Args:
122
+ relay: SocketIORelay instance to configure
123
+ """
124
+ if not self.relay_enabled:
125
+ relay.disable()
126
+ else:
127
+ relay.enable()
128
+
129
+ relay.port = self.relay_port
130
+ relay.debug = self.relay_debug
131
+ relay.max_retries = self.relay_max_retries
132
+ relay.retry_delay = self.relay_retry_delay
133
+ relay.connection_cooldown = self.relay_connection_cooldown
134
+
135
+
136
+ # Global configuration instance
137
+ _config: Optional[EventBusConfig] = None
138
+
139
+
140
+ def get_config() -> EventBusConfig:
141
+ """Get the global EventBus configuration.
142
+
143
+ Returns:
144
+ EventBusConfig: Configuration instance
145
+ """
146
+ global _config
147
+ if _config is None:
148
+ _config = EventBusConfig.from_env()
149
+ return _config
150
+
151
+
152
+ def set_config(config: EventBusConfig) -> None:
153
+ """Set the global EventBus configuration.
154
+
155
+ Args:
156
+ config: Configuration to set
157
+ """
158
+ global _config
159
+ _config = config
160
+
161
+
162
+ def reset_config() -> None:
163
+ """Reset configuration to defaults from environment."""
164
+ global _config
165
+ _config = EventBusConfig.from_env()
@@ -13,7 +13,7 @@ import logging
13
13
  import threading
14
14
  from datetime import datetime
15
15
  from typing import Any, Callable, Dict, List, Optional, Set
16
- from pyee import AsyncIOEventEmitter
16
+ from pyee.asyncio import AsyncIOEventEmitter
17
17
 
18
18
  # Configure logger
19
19
  logger = logging.getLogger(__name__)
@@ -176,9 +176,33 @@ class EventBus:
176
176
  # Record event in history
177
177
  self._record_event(event_type, data)
178
178
 
179
- # Emit event (pyee handles thread safety)
179
+ # Emit event to regular handlers (pyee handles thread safety)
180
180
  self._emitter.emit(event_type, data)
181
181
 
182
+ # Also emit to wildcard handlers
183
+ if hasattr(self, '_wildcard_handlers'):
184
+ for prefix, handlers in self._wildcard_handlers.items():
185
+ if event_type.startswith(prefix):
186
+ for handler in handlers:
187
+ try:
188
+ # Call with event_type and data for wildcard handlers
189
+ if asyncio.iscoroutinefunction(handler):
190
+ # Schedule async handlers
191
+ try:
192
+ loop = asyncio.get_event_loop()
193
+ if loop.is_running():
194
+ asyncio.create_task(handler(event_type, data))
195
+ else:
196
+ loop.run_until_complete(handler(event_type, data))
197
+ except RuntimeError:
198
+ # No event loop, skip async handler
199
+ pass
200
+ else:
201
+ handler(event_type, data)
202
+ except Exception as e:
203
+ if self._debug:
204
+ logger.debug(f"Wildcard handler error: {e}")
205
+
182
206
  # Update stats
183
207
  self._stats["events_published"] += 1
184
208
  self._stats["last_event_time"] = datetime.now().isoformat()
@@ -217,29 +241,20 @@ class EventBus:
217
241
  handler: The handler function
218
242
  """
219
243
  if event_type.endswith("*"):
220
- # Register for wildcard pattern
221
- prefix = event_type[:-1]
244
+ # Store wildcard handlers separately
245
+ if not hasattr(self, '_wildcard_handlers'):
246
+ self._wildcard_handlers = {}
222
247
 
223
- # Create a wrapper that checks event names
224
- def wildcard_wrapper(actual_event_type: str):
225
- def wrapper(data):
226
- if actual_event_type.startswith(prefix):
227
- if asyncio.iscoroutinefunction(handler):
228
- return handler(actual_event_type, data)
229
- else:
230
- handler(actual_event_type, data)
231
- return wrapper
248
+ prefix = event_type[:-1]
249
+ if prefix not in self._wildcard_handlers:
250
+ self._wildcard_handlers[prefix] = []
251
+ self._wildcard_handlers[prefix].append(handler)
232
252
 
233
- # Register for all possible events (we'll filter in the wrapper)
234
- # For now, register common prefixes
235
- for common_event in ["hook", "socketio", "system", "agent"]:
236
- if common_event.startswith(prefix) or prefix.startswith(common_event):
237
- self._emitter.on(f"{common_event}.*", wildcard_wrapper(f"{common_event}.*"))
253
+ logger.debug(f"Registered wildcard handler for: {event_type}")
238
254
  else:
239
255
  # Regular event registration
240
256
  self._emitter.on(event_type, handler)
241
-
242
- logger.debug(f"Registered handler for: {event_type}")
257
+ logger.debug(f"Registered handler for: {event_type}")
243
258
 
244
259
  def once(self, event_type: str, handler: Callable) -> None:
245
260
  """Register a one-time event handler.
@@ -106,20 +106,21 @@ class SocketIORelay:
106
106
  self.last_connection_attempt = current_time
107
107
 
108
108
  try:
109
- # Create new client
109
+ # Create new client with better connection settings
110
110
  self.client = socketio.Client(
111
111
  reconnection=True,
112
- reconnection_attempts=3,
113
- reconnection_delay=1,
112
+ reconnection_attempts=5,
113
+ reconnection_delay=2,
114
+ reconnection_delay_max=10,
114
115
  logger=False,
115
116
  engineio_logger=False
116
117
  )
117
118
 
118
- # Connect to server
119
+ # Connect to server with longer timeout
119
120
  self.client.connect(
120
121
  f"http://localhost:{self.port}",
121
122
  wait=True,
122
- wait_timeout=2.0,
123
+ wait_timeout=10.0, # Increase timeout for stability
123
124
  transports=['websocket', 'polling']
124
125
  )
125
126
 
@@ -219,15 +220,10 @@ class SocketIORelay:
219
220
  if event_type.startswith("hook."):
220
221
  await self.relay_event(event_type, data)
221
222
 
222
- # Subscribe to all hook events
223
+ # Subscribe to all hook events via wildcard
224
+ # This will catch ALL hook.* events
223
225
  self.event_bus.on("hook.*", handle_hook_event)
224
226
 
225
- # Also subscribe to specific high-priority events
226
- for event in ["hook.pre_tool", "hook.post_tool", "hook.subagent_stop",
227
- "hook.user_prompt", "hook.assistant_response"]:
228
- self.event_bus.on(event, lambda data, evt=event:
229
- asyncio.create_task(self.relay_event(evt, data)))
230
-
231
227
  logger.info("SocketIO relay started and subscribed to events")
232
228
 
233
229
  def stop(self) -> None:
@@ -0,0 +1,372 @@
1
+ """
2
+ MCP Gateway Auto-Configuration Service
3
+ ======================================
4
+
5
+ Provides automatic MCP configuration for pipx installations with user consent.
6
+ Detects unconfigured MCP setups and offers one-time configuration prompts.
7
+
8
+ WHY: Users installing via pipx should have MCP work out-of-the-box with minimal
9
+ friction. This service detects unconfigured installations and offers automatic
10
+ setup with user consent.
11
+
12
+ DESIGN DECISIONS:
13
+ - Only prompts once (saves preference to avoid repeated prompts)
14
+ - Quick timeout with safe default (no configuration)
15
+ - Non-intrusive with environment variable override
16
+ - Creates backups before modifying any configuration
17
+ - Validates JSON before and after modifications
18
+ """
19
+
20
+ import json
21
+ import os
22
+ import sys
23
+ import time
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+ from typing import Optional, Dict, Any, Tuple
27
+
28
+ from claude_mpm.core.logger import get_logger
29
+ from claude_mpm.config.paths import paths
30
+
31
+
32
+ class MCPAutoConfigurator:
33
+ """
34
+ Handles automatic MCP configuration for pipx installations.
35
+
36
+ Provides a one-time prompt to configure MCP Gateway with user consent,
37
+ making the experience seamless for pipx users while respecting choice.
38
+ """
39
+
40
+ def __init__(self):
41
+ """Initialize the auto-configurator."""
42
+ self.logger = get_logger("MCPAutoConfig")
43
+ self.config_dir = paths.claude_mpm_dir_hidden
44
+ self.preference_file = self.config_dir / "mcp_auto_config_preference.json"
45
+ self.claude_config_path = Path.home() / ".claude.json"
46
+
47
+ def should_auto_configure(self) -> bool:
48
+ """
49
+ Check if auto-configuration should be attempted.
50
+
51
+ Returns:
52
+ True if auto-configuration should be offered, False otherwise
53
+ """
54
+ # Check environment variable override
55
+ if os.environ.get("CLAUDE_MPM_NO_AUTO_CONFIG"):
56
+ self.logger.debug("Auto-configuration disabled via environment variable")
57
+ return False
58
+
59
+ # Check if already configured
60
+ if self._is_mcp_configured():
61
+ self.logger.debug("MCP already configured")
62
+ return False
63
+
64
+ # Check if this is a pipx installation
65
+ if not self._is_pipx_installation():
66
+ self.logger.debug("Not a pipx installation")
67
+ return False
68
+
69
+ # Check if we've already asked
70
+ if self._has_user_preference():
71
+ self.logger.debug("User preference already saved")
72
+ return False
73
+
74
+ return True
75
+
76
+ def _is_mcp_configured(self) -> bool:
77
+ """Check if MCP is already configured in Claude Code."""
78
+ if not self.claude_config_path.exists():
79
+ return False
80
+
81
+ try:
82
+ with open(self.claude_config_path, 'r') as f:
83
+ config = json.load(f)
84
+
85
+ # Check if claude-mpm-gateway is configured
86
+ mcp_servers = config.get("mcpServers", {})
87
+ return "claude-mpm-gateway" in mcp_servers
88
+
89
+ except (json.JSONDecodeError, IOError):
90
+ return False
91
+
92
+ def _is_pipx_installation(self) -> bool:
93
+ """Check if claude-mpm is installed via pipx."""
94
+ # Check if running from pipx virtual environment
95
+ if "pipx" in sys.executable.lower():
96
+ return True
97
+
98
+ # Check module path
99
+ try:
100
+ import claude_mpm
101
+ module_path = Path(claude_mpm.__file__).parent
102
+ if "pipx" in str(module_path):
103
+ return True
104
+ except Exception:
105
+ pass
106
+
107
+ # Check for pipx in PATH for claude-mpm command
108
+ try:
109
+ import subprocess
110
+ import platform
111
+
112
+ # Use appropriate command for OS
113
+ if platform.system() == "Windows":
114
+ cmd = ["where", "claude-mpm"]
115
+ else:
116
+ cmd = ["which", "claude-mpm"]
117
+
118
+ result = subprocess.run(
119
+ cmd,
120
+ capture_output=True,
121
+ text=True,
122
+ timeout=2
123
+ )
124
+ if result.returncode == 0 and "pipx" in result.stdout:
125
+ return True
126
+ except Exception:
127
+ pass
128
+
129
+ return False
130
+
131
+ def _has_user_preference(self) -> bool:
132
+ """Check if user has already been asked about auto-configuration."""
133
+ if not self.preference_file.exists():
134
+ return False
135
+
136
+ try:
137
+ with open(self.preference_file, 'r') as f:
138
+ prefs = json.load(f)
139
+ return prefs.get("asked", False)
140
+ except (json.JSONDecodeError, IOError):
141
+ return False
142
+
143
+ def _save_user_preference(self, choice: str):
144
+ """Save user's preference to avoid asking again."""
145
+ self.config_dir.mkdir(parents=True, exist_ok=True)
146
+
147
+ prefs = {
148
+ "asked": True,
149
+ "choice": choice,
150
+ "timestamp": datetime.now().isoformat()
151
+ }
152
+
153
+ try:
154
+ with open(self.preference_file, 'w') as f:
155
+ json.dump(prefs, f, indent=2)
156
+ except Exception as e:
157
+ self.logger.debug(f"Could not save preference: {e}")
158
+
159
+ def prompt_user(self, timeout: int = 10) -> Optional[bool]:
160
+ """
161
+ Prompt user for auto-configuration with timeout.
162
+
163
+ Args:
164
+ timeout: Seconds to wait for response (default 10)
165
+
166
+ Returns:
167
+ True if user agrees, False if declines, None if timeout
168
+ """
169
+ print("\n" + "="*60)
170
+ print("🔧 MCP Gateway Configuration")
171
+ print("="*60)
172
+ print("\nClaude MPM can automatically configure MCP Gateway for")
173
+ print("Claude Code integration. This enables advanced features:")
174
+ print(" • File analysis and summarization")
175
+ print(" • System diagnostics")
176
+ print(" • Ticket management")
177
+ print(" • And more...")
178
+ print("\nWould you like to configure it now? (y/n)")
179
+ print(f"(Auto-declining in {timeout} seconds)")
180
+
181
+ # Use threading for cross-platform timeout support
182
+ import threading
183
+ try:
184
+ # Python 3.7+ has queue built-in
185
+ import queue
186
+ except ImportError:
187
+ # Python 2.x fallback
188
+ import Queue as queue
189
+
190
+ user_input = None
191
+
192
+ def get_input():
193
+ nonlocal user_input
194
+ try:
195
+ user_input = input("> ").strip().lower()
196
+ except (EOFError, KeyboardInterrupt):
197
+ user_input = 'n'
198
+
199
+ # Start input thread
200
+ input_thread = threading.Thread(target=get_input)
201
+ input_thread.daemon = True
202
+ input_thread.start()
203
+
204
+ # Wait for input or timeout
205
+ input_thread.join(timeout)
206
+
207
+ if input_thread.is_alive():
208
+ # Timed out
209
+ print("\n(Timed out - declining)")
210
+ return None
211
+ else:
212
+ # Got input
213
+ if user_input in ['y', 'yes']:
214
+ return True
215
+ else:
216
+ return False
217
+
218
+ def auto_configure(self) -> bool:
219
+ """
220
+ Perform automatic MCP configuration.
221
+
222
+ Returns:
223
+ True if configuration successful, False otherwise
224
+ """
225
+ try:
226
+ # Create backup if config exists
227
+ if self.claude_config_path.exists():
228
+ backup_path = self._create_backup()
229
+ if backup_path:
230
+ print(f"✅ Backup created: {backup_path}")
231
+
232
+ # Load or create configuration
233
+ config = self._load_or_create_config()
234
+
235
+ # Add MCP Gateway configuration
236
+ if "mcpServers" not in config:
237
+ config["mcpServers"] = {}
238
+
239
+ # Find claude-mpm executable
240
+ executable = self._find_claude_mpm_executable()
241
+ if not executable:
242
+ print("❌ Could not find claude-mpm executable")
243
+ return False
244
+
245
+ # Configure MCP server
246
+ config["mcpServers"]["claude-mpm-gateway"] = {
247
+ "command": str(executable),
248
+ "args": ["mcp", "server"],
249
+ "env": {
250
+ "MCP_MODE": "production"
251
+ }
252
+ }
253
+
254
+ # Save configuration
255
+ with open(self.claude_config_path, 'w') as f:
256
+ json.dump(config, f, indent=2)
257
+
258
+ print(f"✅ Configuration saved to: {self.claude_config_path}")
259
+ print("\n🎉 MCP Gateway configured successfully!")
260
+ print("\nNext steps:")
261
+ print("1. Restart Claude Code (if running)")
262
+ print("2. Look for the MCP icon in the interface")
263
+ print("3. Try @claude-mpm-gateway in a conversation")
264
+
265
+ return True
266
+
267
+ except Exception as e:
268
+ self.logger.error(f"Auto-configuration failed: {e}")
269
+ print(f"❌ Configuration failed: {e}")
270
+ print("\nYou can configure manually with:")
271
+ print(" claude-mpm mcp install")
272
+ return False
273
+
274
+ def _create_backup(self) -> Optional[Path]:
275
+ """Create backup of existing configuration."""
276
+ try:
277
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
278
+ backup_path = self.claude_config_path.with_suffix(f'.backup.{timestamp}.json')
279
+
280
+ import shutil
281
+ shutil.copy2(self.claude_config_path, backup_path)
282
+ return backup_path
283
+
284
+ except Exception as e:
285
+ self.logger.debug(f"Could not create backup: {e}")
286
+ return None
287
+
288
+ def _load_or_create_config(self) -> Dict[str, Any]:
289
+ """Load existing config or create new one."""
290
+ if self.claude_config_path.exists():
291
+ try:
292
+ with open(self.claude_config_path, 'r') as f:
293
+ return json.load(f)
294
+ except json.JSONDecodeError:
295
+ self.logger.warning("Existing config is invalid JSON, creating new")
296
+
297
+ return {}
298
+
299
+ def _find_claude_mpm_executable(self) -> Optional[str]:
300
+ """Find the claude-mpm executable path."""
301
+ # Try direct command first
302
+ import subprocess
303
+ import platform
304
+
305
+ try:
306
+ # Use appropriate command for OS
307
+ if platform.system() == "Windows":
308
+ cmd = ["where", "claude-mpm"]
309
+ else:
310
+ cmd = ["which", "claude-mpm"]
311
+
312
+ result = subprocess.run(
313
+ cmd,
314
+ capture_output=True,
315
+ text=True,
316
+ timeout=2
317
+ )
318
+ if result.returncode == 0:
319
+ executable_path = result.stdout.strip()
320
+ # On Windows, 'where' might return multiple paths
321
+ if platform.system() == "Windows" and '\n' in executable_path:
322
+ executable_path = executable_path.split('\n')[0]
323
+ return executable_path
324
+ except Exception:
325
+ pass
326
+
327
+ # Try to find via shutil.which (more portable)
328
+ import shutil
329
+ claude_mpm_path = shutil.which("claude-mpm")
330
+ if claude_mpm_path:
331
+ return claude_mpm_path
332
+
333
+ # Fallback to Python module invocation
334
+ return sys.executable
335
+
336
+ def run(self) -> bool:
337
+ """
338
+ Main entry point for auto-configuration.
339
+
340
+ Returns:
341
+ True if configured (or already configured), False otherwise
342
+ """
343
+ if not self.should_auto_configure():
344
+ return True # Already configured or not applicable
345
+
346
+ # Prompt user
347
+ user_choice = self.prompt_user()
348
+
349
+ # Save preference to not ask again
350
+ self._save_user_preference("yes" if user_choice else "no")
351
+
352
+ if user_choice:
353
+ return self.auto_configure()
354
+ else:
355
+ if user_choice is False: # User explicitly said no
356
+ print("\n📝 You can configure MCP later with:")
357
+ print(" claude-mpm mcp install")
358
+ # If timeout (None), don't show additional message
359
+ return False
360
+
361
+
362
+ def check_and_configure_mcp() -> bool:
363
+ """
364
+ Check and potentially configure MCP for pipx installations.
365
+
366
+ This is the main entry point called during CLI initialization.
367
+
368
+ Returns:
369
+ True if MCP is configured (or configuration was successful), False otherwise
370
+ """
371
+ configurator = MCPAutoConfigurator()
372
+ return configurator.run()
@@ -99,9 +99,9 @@ class ConnectionEventHandler(BaseEventHandler):
99
99
  # Connection health tracking
100
100
  self.connection_metrics = {}
101
101
  self.last_ping_times = {}
102
- self.ping_interval = 30 # seconds
103
- self.ping_timeout = 10 # seconds
104
- self.stale_check_interval = 60 # seconds
102
+ self.ping_interval = 45 # seconds - avoid conflict with Engine.IO pings
103
+ self.ping_timeout = 20 # seconds - more lenient timeout
104
+ self.stale_check_interval = 90 # seconds - less frequent checks
105
105
 
106
106
  # Health monitoring tasks (will be started after event registration)
107
107
  self.ping_task = None