claude-mpm 4.0.34__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.
Files changed (33) 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/cli/__init__.py +48 -7
  6. claude_mpm/cli/commands/agents.py +82 -0
  7. claude_mpm/cli/commands/cleanup_orphaned_agents.py +150 -0
  8. claude_mpm/cli/commands/mcp_pipx_config.py +199 -0
  9. claude_mpm/cli/parsers/agents_parser.py +27 -0
  10. claude_mpm/cli/parsers/base_parser.py +6 -0
  11. claude_mpm/cli/startup_logging.py +75 -0
  12. claude_mpm/dashboard/static/js/components/build-tracker.js +35 -1
  13. claude_mpm/dashboard/static/js/socket-client.js +7 -5
  14. claude_mpm/hooks/claude_hooks/connection_pool.py +13 -2
  15. claude_mpm/hooks/claude_hooks/hook_handler.py +67 -167
  16. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -1
  17. claude_mpm/services/agents/deployment/agent_template_builder.py +2 -1
  18. claude_mpm/services/agents/deployment/agent_version_manager.py +4 -1
  19. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +207 -10
  20. claude_mpm/services/event_bus/config.py +165 -0
  21. claude_mpm/services/event_bus/event_bus.py +35 -20
  22. claude_mpm/services/event_bus/relay.py +8 -12
  23. claude_mpm/services/mcp_gateway/auto_configure.py +372 -0
  24. claude_mpm/services/socketio/handlers/connection.py +3 -3
  25. claude_mpm/services/socketio/server/core.py +25 -2
  26. claude_mpm/services/socketio/server/eventbus_integration.py +189 -0
  27. claude_mpm/services/socketio/server/main.py +25 -0
  28. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/METADATA +25 -7
  29. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/RECORD +33 -28
  30. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/WHEEL +0 -0
  31. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/entry_points.txt +0 -0
  32. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/licenses/LICENSE +0 -0
  33. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/top_level.txt +0 -0
@@ -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
@@ -31,6 +31,9 @@ except ImportError:
31
31
  aiohttp = None
32
32
  web = None
33
33
 
34
+ # Import VersionService for dynamic version retrieval
35
+ from claude_mpm.services.version_service import VersionService
36
+
34
37
  from ....core.constants import (
35
38
  NetworkConfig,
36
39
  PerformanceConfig,
@@ -158,11 +161,14 @@ class SocketIOServerCore:
158
161
  async def _start_server(self):
159
162
  """Start the Socket.IO server with aiohttp."""
160
163
  try:
161
- # Create Socket.IO server
164
+ # Create Socket.IO server with proper ping/pong configuration
162
165
  self.sio = socketio.AsyncServer(
163
166
  cors_allowed_origins="*",
164
167
  logger=False, # Disable Socket.IO's own logging
165
168
  engineio_logger=False,
169
+ ping_interval=25, # Send ping every 25 seconds
170
+ ping_timeout=60, # Wait 60 seconds for pong response
171
+ max_http_buffer_size=1e8, # 100MB max buffer
166
172
  )
167
173
 
168
174
  # Create aiohttp application
@@ -255,6 +261,23 @@ class SocketIOServerCore:
255
261
  return web.Response(text="Dashboard not available", status=404)
256
262
 
257
263
  self.app.router.add_get("/", index_handler)
264
+
265
+ # Serve version.json from dashboard directory
266
+ async def version_handler(request):
267
+ version_file = self.dashboard_path / "version.json"
268
+ if version_file.exists():
269
+ self.logger.debug(f"Serving version.json from: {version_file}")
270
+ return web.FileResponse(version_file)
271
+ else:
272
+ # Return default version info if file doesn't exist
273
+ return web.json_response({
274
+ "version": "1.0.0",
275
+ "build": 1,
276
+ "formatted_build": "0001",
277
+ "full_version": "v1.0.0-0001"
278
+ })
279
+
280
+ self.app.router.add_get("/version.json", version_handler)
258
281
 
259
282
  # Serve static assets (CSS, JS) from the dashboard static directory
260
283
  dashboard_static_path = (
@@ -426,7 +449,7 @@ class SocketIOServerCore:
426
449
  "total_events": self.stats.get("events_sent", 0),
427
450
  "active_sessions": active_sessions,
428
451
  "server_info": {
429
- "version": "4.0.2",
452
+ "version": VersionService().get_version(),
430
453
  "port": self.port,
431
454
  },
432
455
  },
@@ -0,0 +1,189 @@
1
+ """EventBus integration for Socket.IO server.
2
+
3
+ WHY this integration module:
4
+ - Adds EventBus relay to Socket.IO server without modifying core
5
+ - Maintains backward compatibility with existing server
6
+ - Easy to enable/disable via configuration
7
+ - Provides clean separation of concerns
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+ from typing import Optional
13
+
14
+ from claude_mpm.services.event_bus import EventBus, SocketIORelay
15
+ from claude_mpm.services.event_bus.config import get_config
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class EventBusIntegration:
21
+ """Integrates EventBus relay with Socket.IO server.
22
+
23
+ WHY integration class:
24
+ - Encapsulates EventBus setup and teardown
25
+ - Provides lifecycle management for relay
26
+ - Handles configuration and error cases
27
+ - Can be easily added to existing server
28
+ """
29
+
30
+ def __init__(self, server_instance=None):
31
+ """Initialize EventBus integration.
32
+
33
+ Args:
34
+ server_instance: Optional Socket.IO server instance
35
+ """
36
+ self.server = server_instance
37
+ self.relay: Optional[SocketIORelay] = None
38
+ self.event_bus: Optional[EventBus] = None
39
+ self.config = get_config()
40
+ self.enabled = self.config.enabled and self.config.relay_enabled
41
+
42
+ def setup(self, port: Optional[int] = None) -> bool:
43
+ """Set up EventBus and relay.
44
+
45
+ Args:
46
+ port: Optional Socket.IO server port
47
+
48
+ Returns:
49
+ bool: True if setup successful
50
+ """
51
+ if not self.enabled:
52
+ logger.info("EventBus integration disabled by configuration")
53
+ return False
54
+
55
+ try:
56
+ # Get EventBus instance
57
+ self.event_bus = EventBus.get_instance()
58
+
59
+ # Apply configuration
60
+ self.config.apply_to_eventbus(self.event_bus)
61
+
62
+ # Create and configure relay
63
+ relay_port = port or self.config.relay_port
64
+ self.relay = SocketIORelay(relay_port)
65
+ self.config.apply_to_relay(self.relay)
66
+
67
+ # Start the relay
68
+ self.relay.start()
69
+
70
+ logger.info(f"EventBus integration setup complete (port: {relay_port})")
71
+ return True
72
+
73
+ except Exception as e:
74
+ logger.error(f"Failed to setup EventBus integration: {e}")
75
+ self.enabled = False
76
+ return False
77
+
78
+ def teardown(self) -> None:
79
+ """Tear down EventBus integration."""
80
+ if self.relay:
81
+ try:
82
+ self.relay.stop()
83
+ logger.info("EventBus relay stopped")
84
+ except Exception as e:
85
+ logger.error(f"Error stopping relay: {e}")
86
+ finally:
87
+ self.relay = None
88
+
89
+ def is_active(self) -> bool:
90
+ """Check if integration is active.
91
+
92
+ Returns:
93
+ bool: True if relay is active and connected
94
+ """
95
+ return (
96
+ self.enabled and
97
+ self.relay is not None and
98
+ self.relay.enabled
99
+ )
100
+
101
+ def get_stats(self) -> dict:
102
+ """Get integration statistics.
103
+
104
+ Returns:
105
+ dict: Combined stats from EventBus and relay
106
+ """
107
+ stats = {
108
+ "enabled": self.enabled,
109
+ "active": self.is_active()
110
+ }
111
+
112
+ if self.event_bus:
113
+ stats["eventbus"] = self.event_bus.get_stats()
114
+
115
+ if self.relay:
116
+ stats["relay"] = self.relay.get_stats()
117
+
118
+ return stats
119
+
120
+
121
+ def integrate_with_server(server_instance, port: Optional[int] = None) -> EventBusIntegration:
122
+ """Helper function to integrate EventBus with a Socket.IO server.
123
+
124
+ Args:
125
+ server_instance: Socket.IO server instance
126
+ port: Optional server port
127
+
128
+ Returns:
129
+ EventBusIntegration: The integration instance
130
+ """
131
+ integration = EventBusIntegration(server_instance)
132
+ integration.setup(port or getattr(server_instance, 'port', 8765))
133
+ return integration
134
+
135
+
136
+ # Monkey-patch helper for existing server
137
+ def patch_socketio_server(server_class):
138
+ """Monkey-patch an existing Socket.IO server class to add EventBus.
139
+
140
+ WHY monkey-patching:
141
+ - Allows integration without modifying existing code
142
+ - Can be applied selectively based on configuration
143
+ - Easy to remove or disable
144
+
145
+ Args:
146
+ server_class: The server class to patch
147
+ """
148
+ original_init = server_class.__init__
149
+ original_start = getattr(server_class, 'start_sync', None) or getattr(server_class, 'start', None)
150
+ original_stop = getattr(server_class, 'stop_sync', None) or getattr(server_class, 'stop', None)
151
+
152
+ def patched_init(self, *args, **kwargs):
153
+ """Patched __init__ that adds EventBus integration."""
154
+ original_init(self, *args, **kwargs)
155
+ self._eventbus_integration = EventBusIntegration(self)
156
+
157
+ def patched_start(self, *args, **kwargs):
158
+ """Patched start method that sets up EventBus."""
159
+ # Call original start
160
+ if original_start:
161
+ result = original_start(self, *args, **kwargs)
162
+ else:
163
+ result = None
164
+
165
+ # Setup EventBus integration
166
+ if hasattr(self, '_eventbus_integration'):
167
+ port = getattr(self, 'port', 8765)
168
+ self._eventbus_integration.setup(port)
169
+
170
+ return result
171
+
172
+ def patched_stop(self, *args, **kwargs):
173
+ """Patched stop method that tears down EventBus."""
174
+ # Teardown EventBus first
175
+ if hasattr(self, '_eventbus_integration'):
176
+ self._eventbus_integration.teardown()
177
+
178
+ # Call original stop
179
+ if original_stop:
180
+ return original_stop(self, *args, **kwargs)
181
+
182
+ # Apply patches
183
+ server_class.__init__ = patched_init
184
+ if original_start:
185
+ setattr(server_class, 'start_sync' if hasattr(server_class, 'start_sync') else 'start', patched_start)
186
+ if original_stop:
187
+ setattr(server_class, 'stop_sync' if hasattr(server_class, 'stop_sync') else 'stop', patched_stop)
188
+
189
+ logger.info(f"Patched {server_class.__name__} with EventBus integration")
@@ -43,6 +43,7 @@ from ...exceptions import SocketIOServerError as MPMConnectionError
43
43
  from ..handlers import EventHandlerRegistry, FileEventHandler, GitEventHandler
44
44
  from .broadcaster import SocketIOEventBroadcaster
45
45
  from .core import SocketIOServerCore
46
+ from .eventbus_integration import EventBusIntegration
46
47
 
47
48
 
48
49
  class SocketIOServer(SocketIOServiceInterface):
@@ -93,6 +94,9 @@ class SocketIOServer(SocketIOServiceInterface):
93
94
 
94
95
  # Active session tracking for heartbeat
95
96
  self.active_sessions: Dict[str, Dict[str, Any]] = {}
97
+
98
+ # EventBus integration
99
+ self.eventbus_integration = None
96
100
 
97
101
  def start_sync(self):
98
102
  """Start the Socket.IO server in a background thread (synchronous version)."""
@@ -144,6 +148,19 @@ class SocketIOServer(SocketIOServiceInterface):
144
148
 
145
149
  # Register events
146
150
  self._register_events()
151
+
152
+ # Setup EventBus integration
153
+ # WHY: This connects the EventBus to the Socket.IO server, allowing
154
+ # events from other parts of the system to be broadcast to dashboard
155
+ try:
156
+ self.eventbus_integration = EventBusIntegration(self)
157
+ if self.eventbus_integration.setup(self.port):
158
+ self.logger.info("EventBus integration setup successful")
159
+ else:
160
+ self.logger.warning("EventBus integration setup failed or disabled")
161
+ except Exception as e:
162
+ self.logger.error(f"Failed to setup EventBus integration: {e}")
163
+ self.eventbus_integration = None
147
164
 
148
165
  # Update running state
149
166
  self.running = self.core.running
@@ -159,6 +176,14 @@ class SocketIOServer(SocketIOServiceInterface):
159
176
  if self.broadcaster:
160
177
  self.broadcaster.stop_retry_processor()
161
178
 
179
+ # Teardown EventBus integration
180
+ if self.eventbus_integration:
181
+ try:
182
+ self.eventbus_integration.teardown()
183
+ self.logger.info("EventBus integration teardown complete")
184
+ except Exception as e:
185
+ self.logger.error(f"Error during EventBus teardown: {e}")
186
+
162
187
  # Stop health monitoring in connection handler
163
188
  if self.event_registry:
164
189
  from ..handlers import ConnectionEventHandler