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.
- 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/web_qa.json +85 -58
- claude_mpm/agents/templates/web_ui.json +3 -3
- claude_mpm/cli/__init__.py +48 -7
- 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/dashboard/static/js/components/build-tracker.js +35 -1
- claude_mpm/dashboard/static/js/socket-client.js +7 -5
- claude_mpm/hooks/claude_hooks/connection_pool.py +13 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +67 -167
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -1
- claude_mpm/services/agents/deployment/agent_template_builder.py +2 -1
- claude_mpm/services/agents/deployment/agent_version_manager.py +4 -1
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +207 -10
- claude_mpm/services/event_bus/config.py +165 -0
- claude_mpm/services/event_bus/event_bus.py +35 -20
- claude_mpm/services/event_bus/relay.py +8 -12
- claude_mpm/services/mcp_gateway/auto_configure.py +372 -0
- claude_mpm/services/socketio/handlers/connection.py +3 -3
- claude_mpm/services/socketio/server/core.py +25 -2
- claude_mpm/services/socketio/server/eventbus_integration.py +189 -0
- claude_mpm/services/socketio/server/main.py +25 -0
- {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/METADATA +25 -7
- {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/RECORD +35 -30
- {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
#
|
|
221
|
-
|
|
244
|
+
# Store wildcard handlers separately
|
|
245
|
+
if not hasattr(self, '_wildcard_handlers'):
|
|
246
|
+
self._wildcard_handlers = {}
|
|
222
247
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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=
|
|
113
|
-
reconnection_delay=
|
|
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=
|
|
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 =
|
|
103
|
-
self.ping_timeout =
|
|
104
|
-
self.stale_check_interval =
|
|
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
|