claude-mpm 4.0.17__py3-none-any.whl → 4.0.20__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/__main__.py +4 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +38 -2
- claude_mpm/agents/OUTPUT_STYLE.md +84 -0
- claude_mpm/agents/templates/qa.json +24 -12
- claude_mpm/cli/__init__.py +85 -1
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/commands/mcp_install_commands.py +62 -5
- claude_mpm/cli/commands/mcp_server_commands.py +60 -79
- claude_mpm/cli/commands/memory.py +32 -5
- claude_mpm/cli/commands/run.py +33 -6
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/cli/parsers/run_parser.py +5 -0
- claude_mpm/cli/utils.py +17 -4
- claude_mpm/core/base_service.py +1 -1
- claude_mpm/core/config.py +70 -5
- claude_mpm/core/framework_loader.py +342 -31
- claude_mpm/core/interactive_session.py +55 -1
- claude_mpm/core/oneshot_session.py +7 -1
- claude_mpm/core/output_style_manager.py +468 -0
- claude_mpm/core/unified_paths.py +190 -21
- claude_mpm/hooks/claude_hooks/hook_handler.py +91 -16
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +3 -0
- claude_mpm/init.py +1 -0
- claude_mpm/scripts/mcp_server.py +68 -0
- claude_mpm/scripts/mcp_wrapper.py +39 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +151 -7
- claude_mpm/services/agents/deployment/agent_template_builder.py +37 -1
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +441 -0
- claude_mpm/services/agents/memory/__init__.py +0 -2
- claude_mpm/services/agents/memory/agent_memory_manager.py +737 -43
- claude_mpm/services/agents/memory/content_manager.py +144 -14
- claude_mpm/services/agents/memory/template_generator.py +7 -354
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +312 -0
- claude_mpm/services/mcp_gateway/core/startup_verification.py +315 -0
- claude_mpm/services/mcp_gateway/main.py +7 -0
- claude_mpm/services/mcp_gateway/server/stdio_server.py +184 -176
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +453 -0
- claude_mpm/services/subprocess_launcher_service.py +5 -0
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/METADATA +1 -1
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/RECORD +45 -38
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/entry_points.txt +1 -0
- claude_mpm/services/agents/memory/analyzer.py +0 -430
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/top_level.txt +0 -0
claude_mpm/core/unified_paths.py
CHANGED
|
@@ -64,42 +64,179 @@ class DeploymentContext(Enum):
|
|
|
64
64
|
class PathContext:
|
|
65
65
|
"""Handles deployment context detection and path resolution."""
|
|
66
66
|
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _is_editable_install() -> bool:
|
|
69
|
+
"""Check if the current installation is editable (development mode).
|
|
70
|
+
|
|
71
|
+
This checks for various indicators of an editable/development installation:
|
|
72
|
+
- Presence of pyproject.toml in parent directories
|
|
73
|
+
- .pth files pointing to the source directory
|
|
74
|
+
- Direct source installation (src/ directory structure)
|
|
75
|
+
- Current working directory is within a development project
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
import claude_mpm
|
|
79
|
+
module_path = Path(claude_mpm.__file__).parent
|
|
80
|
+
|
|
81
|
+
# Check if we're in a src/ directory structure with pyproject.toml
|
|
82
|
+
current = module_path
|
|
83
|
+
for _ in range(5): # Check up to 5 levels up
|
|
84
|
+
if (current / "pyproject.toml").exists():
|
|
85
|
+
# Found pyproject.toml, check if this looks like a development setup
|
|
86
|
+
if (current / "src" / "claude_mpm").exists():
|
|
87
|
+
logger.debug(f"Found development installation at {current}")
|
|
88
|
+
return True
|
|
89
|
+
if current == current.parent:
|
|
90
|
+
break
|
|
91
|
+
current = current.parent
|
|
92
|
+
|
|
93
|
+
# Additional check: If we're running from within a claude-mpm development directory
|
|
94
|
+
# This handles the case where pipx claude-mpm is invoked from within the dev directory
|
|
95
|
+
cwd = Path.cwd()
|
|
96
|
+
current = cwd
|
|
97
|
+
for _ in range(5): # Check up to 5 levels up from current directory
|
|
98
|
+
if (current / "pyproject.toml").exists() and (current / "src" / "claude_mpm").exists():
|
|
99
|
+
# Check if this is the claude-mpm project
|
|
100
|
+
try:
|
|
101
|
+
pyproject_content = (current / "pyproject.toml").read_text()
|
|
102
|
+
if "claude-mpm" in pyproject_content and "claude_mpm" in pyproject_content:
|
|
103
|
+
logger.debug(f"Running from within claude-mpm development directory: {current}")
|
|
104
|
+
# Verify this is a development setup by checking for key files
|
|
105
|
+
if (current / "scripts" / "claude-mpm").exists():
|
|
106
|
+
return True
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
if current == current.parent:
|
|
110
|
+
break
|
|
111
|
+
current = current.parent
|
|
112
|
+
|
|
113
|
+
# Check for .pth files indicating editable install
|
|
114
|
+
try:
|
|
115
|
+
import site
|
|
116
|
+
for site_dir in site.getsitepackages():
|
|
117
|
+
site_path = Path(site_dir)
|
|
118
|
+
if site_path.exists():
|
|
119
|
+
# Check for .pth files
|
|
120
|
+
for pth_file in site_path.glob("*.pth"):
|
|
121
|
+
try:
|
|
122
|
+
content = pth_file.read_text()
|
|
123
|
+
# Check if the .pth file points to our module's parent
|
|
124
|
+
if str(module_path.parent) in content or str(module_path) in content:
|
|
125
|
+
logger.debug(f"Found editable install via .pth file: {pth_file}")
|
|
126
|
+
return True
|
|
127
|
+
except Exception:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# Check for egg-link files
|
|
131
|
+
for egg_link in site_path.glob("*egg-link"):
|
|
132
|
+
if "claude" in egg_link.name.lower():
|
|
133
|
+
try:
|
|
134
|
+
content = egg_link.read_text()
|
|
135
|
+
if str(module_path.parent) in content or str(module_path) in content:
|
|
136
|
+
logger.debug(f"Found editable install via egg-link: {egg_link}")
|
|
137
|
+
return True
|
|
138
|
+
except Exception:
|
|
139
|
+
continue
|
|
140
|
+
except ImportError:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.debug(f"Error checking for editable install: {e}")
|
|
145
|
+
|
|
146
|
+
return False
|
|
147
|
+
|
|
67
148
|
@staticmethod
|
|
68
149
|
@lru_cache(maxsize=1)
|
|
69
150
|
def detect_deployment_context() -> DeploymentContext:
|
|
70
|
-
"""Detect the current deployment context.
|
|
151
|
+
"""Detect the current deployment context.
|
|
152
|
+
|
|
153
|
+
Priority order:
|
|
154
|
+
1. Environment variable override (CLAUDE_MPM_DEV_MODE)
|
|
155
|
+
2. Current working directory is a claude-mpm development project
|
|
156
|
+
3. Editable installation detection
|
|
157
|
+
4. Path-based detection (development, pipx, system, pip)
|
|
158
|
+
"""
|
|
159
|
+
# Check for environment variable override
|
|
160
|
+
if os.environ.get("CLAUDE_MPM_DEV_MODE", "").lower() in ("1", "true", "yes"):
|
|
161
|
+
logger.info("Development mode forced via CLAUDE_MPM_DEV_MODE environment variable")
|
|
162
|
+
return DeploymentContext.DEVELOPMENT
|
|
163
|
+
|
|
164
|
+
# Check if current working directory is a claude-mpm development project
|
|
165
|
+
# This handles the case where pipx claude-mpm is run from within the dev directory
|
|
166
|
+
cwd = Path.cwd()
|
|
167
|
+
current = cwd
|
|
168
|
+
for _ in range(5): # Check up to 5 levels up from current directory
|
|
169
|
+
if (current / "pyproject.toml").exists() and (current / "src" / "claude_mpm").exists():
|
|
170
|
+
# Check if this is the claude-mpm project
|
|
171
|
+
try:
|
|
172
|
+
pyproject_content = (current / "pyproject.toml").read_text()
|
|
173
|
+
if 'name = "claude-mpm"' in pyproject_content or '"claude-mpm"' in pyproject_content:
|
|
174
|
+
logger.info(f"Detected claude-mpm development directory at {current}")
|
|
175
|
+
logger.info("Using development mode for local source preference")
|
|
176
|
+
return DeploymentContext.DEVELOPMENT
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
if current == current.parent:
|
|
180
|
+
break
|
|
181
|
+
current = current.parent
|
|
182
|
+
|
|
71
183
|
try:
|
|
72
184
|
import claude_mpm
|
|
73
|
-
|
|
74
185
|
module_path = Path(claude_mpm.__file__).parent
|
|
75
|
-
|
|
76
|
-
#
|
|
186
|
+
|
|
187
|
+
# First check if this is an editable install, regardless of path
|
|
188
|
+
# This is important for cases where pipx points to a development installation
|
|
189
|
+
if PathContext._is_editable_install():
|
|
190
|
+
logger.info(f"Detected editable/development installation")
|
|
191
|
+
# Check if we should use development paths
|
|
192
|
+
# This could be because we're in a src/ directory or running from dev directory
|
|
193
|
+
if module_path.parent.name == "src":
|
|
194
|
+
return DeploymentContext.DEVELOPMENT
|
|
195
|
+
elif "pipx" in str(module_path):
|
|
196
|
+
# Running via pipx but from within a development directory
|
|
197
|
+
# Use development mode to prefer local source over pipx installation
|
|
198
|
+
cwd = Path.cwd()
|
|
199
|
+
current = cwd
|
|
200
|
+
for _ in range(5):
|
|
201
|
+
if (current / "src" / "claude_mpm").exists() and (current / "pyproject.toml").exists():
|
|
202
|
+
logger.info(f"Running pipx from development directory, using development mode")
|
|
203
|
+
return DeploymentContext.DEVELOPMENT
|
|
204
|
+
if current == current.parent:
|
|
205
|
+
break
|
|
206
|
+
current = current.parent
|
|
207
|
+
return DeploymentContext.EDITABLE_INSTALL
|
|
208
|
+
else:
|
|
209
|
+
return DeploymentContext.EDITABLE_INSTALL
|
|
210
|
+
|
|
211
|
+
# Check for development mode based on directory structure
|
|
77
212
|
# module_path is typically /path/to/project/src/claude_mpm
|
|
78
|
-
# So we need to check if /path/to/project/src exists (module_path.parent)
|
|
79
|
-
# and if /path/to/project/src/claude_mpm exists (module_path itself)
|
|
80
213
|
if (module_path.parent.name == "src" and
|
|
81
214
|
(module_path.parent.parent / "src" / "claude_mpm").exists()):
|
|
215
|
+
logger.info(f"Detected development mode via directory structure at {module_path}")
|
|
82
216
|
return DeploymentContext.DEVELOPMENT
|
|
83
217
|
|
|
84
|
-
# Check for editable install
|
|
85
|
-
if (
|
|
86
|
-
"site-packages" in str(module_path)
|
|
87
|
-
and (module_path.parent.parent / "src").exists()
|
|
88
|
-
):
|
|
89
|
-
return DeploymentContext.EDITABLE_INSTALL
|
|
90
|
-
|
|
91
218
|
# Check for pipx install
|
|
92
219
|
if "pipx" in str(module_path):
|
|
220
|
+
logger.info(f"Detected pipx installation at {module_path}")
|
|
93
221
|
return DeploymentContext.PIPX_INSTALL
|
|
94
222
|
|
|
95
223
|
# Check for system package
|
|
96
224
|
if "dist-packages" in str(module_path):
|
|
225
|
+
logger.info(f"Detected system package installation at {module_path}")
|
|
97
226
|
return DeploymentContext.SYSTEM_PACKAGE
|
|
227
|
+
|
|
228
|
+
# Check for site-packages (could be pip or editable)
|
|
229
|
+
if "site-packages" in str(module_path):
|
|
230
|
+
# Already checked for editable above, so this is a regular pip install
|
|
231
|
+
logger.info(f"Detected pip installation at {module_path}")
|
|
232
|
+
return DeploymentContext.PIP_INSTALL
|
|
98
233
|
|
|
99
234
|
# Default to pip install
|
|
235
|
+
logger.info(f"Defaulting to pip installation for {module_path}")
|
|
100
236
|
return DeploymentContext.PIP_INSTALL
|
|
101
237
|
|
|
102
238
|
except ImportError:
|
|
239
|
+
logger.info("ImportError during context detection, defaulting to development")
|
|
103
240
|
return DeploymentContext.DEVELOPMENT
|
|
104
241
|
|
|
105
242
|
|
|
@@ -143,8 +280,8 @@ class UnifiedPathManager:
|
|
|
143
280
|
]
|
|
144
281
|
self._initialized = True
|
|
145
282
|
|
|
146
|
-
logger.
|
|
147
|
-
f"UnifiedPathManager initialized with context: {self._deployment_context}"
|
|
283
|
+
logger.info(
|
|
284
|
+
f"UnifiedPathManager initialized with context: {self._deployment_context.value}"
|
|
148
285
|
)
|
|
149
286
|
|
|
150
287
|
# ========================================================================
|
|
@@ -160,11 +297,38 @@ class UnifiedPathManager:
|
|
|
160
297
|
|
|
161
298
|
module_path = Path(claude_mpm.__file__).parent
|
|
162
299
|
|
|
163
|
-
if self._deployment_context
|
|
164
|
-
#
|
|
300
|
+
if self._deployment_context in (DeploymentContext.DEVELOPMENT, DeploymentContext.EDITABLE_INSTALL):
|
|
301
|
+
# For development mode, first check if we're running from within a dev directory
|
|
302
|
+
# This handles the case where pipx is invoked from a development directory
|
|
303
|
+
cwd = Path.cwd()
|
|
304
|
+
current = cwd
|
|
305
|
+
for _ in range(5):
|
|
306
|
+
if (current / "src" / "claude_mpm").exists() and (current / "pyproject.toml").exists():
|
|
307
|
+
# Verify this is the claude-mpm project
|
|
308
|
+
try:
|
|
309
|
+
pyproject_content = (current / "pyproject.toml").read_text()
|
|
310
|
+
if "claude-mpm" in pyproject_content:
|
|
311
|
+
logger.debug(f"Found framework root via cwd at {current}")
|
|
312
|
+
return current
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
if current == current.parent:
|
|
316
|
+
break
|
|
317
|
+
current = current.parent
|
|
318
|
+
|
|
319
|
+
# Development or editable install: go up to project root from module
|
|
165
320
|
current = module_path
|
|
166
321
|
while current != current.parent:
|
|
167
|
-
if (current / "src" / "claude_mpm").exists():
|
|
322
|
+
if (current / "src" / "claude_mpm").exists() and (current / "pyproject.toml").exists():
|
|
323
|
+
logger.debug(f"Found framework root at {current}")
|
|
324
|
+
return current
|
|
325
|
+
current = current.parent
|
|
326
|
+
|
|
327
|
+
# Secondary check: Look for pyproject.toml without src structure
|
|
328
|
+
current = module_path
|
|
329
|
+
while current != current.parent:
|
|
330
|
+
if (current / "pyproject.toml").exists():
|
|
331
|
+
logger.debug(f"Found framework root (no src) at {current}")
|
|
168
332
|
return current
|
|
169
333
|
current = current.parent
|
|
170
334
|
|
|
@@ -202,9 +366,14 @@ class UnifiedPathManager:
|
|
|
202
366
|
@property
|
|
203
367
|
def package_root(self) -> Path:
|
|
204
368
|
"""Get the claude_mpm package root directory."""
|
|
369
|
+
if self._deployment_context in (DeploymentContext.DEVELOPMENT, DeploymentContext.EDITABLE_INSTALL):
|
|
370
|
+
# In development mode, always use the source directory
|
|
371
|
+
package_path = self.framework_root / "src" / "claude_mpm"
|
|
372
|
+
if package_path.exists():
|
|
373
|
+
return package_path
|
|
374
|
+
|
|
205
375
|
try:
|
|
206
376
|
import claude_mpm
|
|
207
|
-
|
|
208
377
|
return Path(claude_mpm.__file__).parent
|
|
209
378
|
except ImportError:
|
|
210
379
|
return self.framework_root / "src" / "claude_mpm"
|
|
@@ -246,7 +415,7 @@ class UnifiedPathManager:
|
|
|
246
415
|
elif scope == "project":
|
|
247
416
|
return self.get_project_config_dir() / "agents"
|
|
248
417
|
elif scope == "framework":
|
|
249
|
-
if self._deployment_context
|
|
418
|
+
if self._deployment_context in (DeploymentContext.DEVELOPMENT, DeploymentContext.EDITABLE_INSTALL):
|
|
250
419
|
return self.framework_root / "src" / "claude_mpm" / "agents"
|
|
251
420
|
else:
|
|
252
421
|
return self.package_root / "agents"
|
|
@@ -277,7 +446,7 @@ class UnifiedPathManager:
|
|
|
277
446
|
|
|
278
447
|
def get_scripts_dir(self) -> Path:
|
|
279
448
|
"""Get the scripts directory."""
|
|
280
|
-
if self._deployment_context
|
|
449
|
+
if self._deployment_context in (DeploymentContext.DEVELOPMENT, DeploymentContext.EDITABLE_INSTALL):
|
|
281
450
|
return self.framework_root / "scripts"
|
|
282
451
|
else:
|
|
283
452
|
return self.package_root / "scripts"
|
|
@@ -65,6 +65,10 @@ except ImportError:
|
|
|
65
65
|
_global_handler = None
|
|
66
66
|
_handler_lock = threading.Lock()
|
|
67
67
|
|
|
68
|
+
# Track recent events to detect duplicates
|
|
69
|
+
_recent_events = deque(maxlen=10)
|
|
70
|
+
_events_lock = threading.Lock()
|
|
71
|
+
|
|
68
72
|
|
|
69
73
|
class ClaudeHookHandler:
|
|
70
74
|
"""Optimized hook handler with direct Socket.IO client.
|
|
@@ -288,12 +292,16 @@ class ClaudeHookHandler:
|
|
|
288
292
|
- Always continues regardless of event status
|
|
289
293
|
- Process exits after handling to prevent accumulation
|
|
290
294
|
"""
|
|
295
|
+
_continue_sent = False # Track if continue has been sent
|
|
291
296
|
|
|
292
297
|
def timeout_handler(signum, frame):
|
|
293
298
|
"""Handle timeout by forcing exit."""
|
|
299
|
+
nonlocal _continue_sent
|
|
294
300
|
if DEBUG:
|
|
295
301
|
print(f"Hook handler timeout (pid: {os.getpid()})", file=sys.stderr)
|
|
296
|
-
|
|
302
|
+
if not _continue_sent:
|
|
303
|
+
self._continue_execution()
|
|
304
|
+
_continue_sent = True
|
|
297
305
|
sys.exit(0)
|
|
298
306
|
|
|
299
307
|
try:
|
|
@@ -304,8 +312,35 @@ class ClaudeHookHandler:
|
|
|
304
312
|
# Read and parse event
|
|
305
313
|
event = self._read_hook_event()
|
|
306
314
|
if not event:
|
|
307
|
-
|
|
315
|
+
if not _continue_sent:
|
|
316
|
+
self._continue_execution()
|
|
317
|
+
_continue_sent = True
|
|
308
318
|
return
|
|
319
|
+
|
|
320
|
+
# Check for duplicate events (same event within 100ms)
|
|
321
|
+
global _recent_events, _events_lock
|
|
322
|
+
event_key = self._get_event_key(event)
|
|
323
|
+
current_time = time.time()
|
|
324
|
+
|
|
325
|
+
with _events_lock:
|
|
326
|
+
# Check if we've seen this event recently
|
|
327
|
+
for recent_key, recent_time in _recent_events:
|
|
328
|
+
if recent_key == event_key and (current_time - recent_time) < 0.1:
|
|
329
|
+
if DEBUG:
|
|
330
|
+
print(f"[{datetime.now().isoformat()}] Skipping duplicate event: {event.get('hook_event_name', 'unknown')} (PID: {os.getpid()})", file=sys.stderr)
|
|
331
|
+
# Still need to output continue for this invocation
|
|
332
|
+
if not _continue_sent:
|
|
333
|
+
self._continue_execution()
|
|
334
|
+
_continue_sent = True
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# Not a duplicate, record it
|
|
338
|
+
_recent_events.append((event_key, current_time))
|
|
339
|
+
|
|
340
|
+
# Debug: Log that we're processing an event
|
|
341
|
+
if DEBUG:
|
|
342
|
+
hook_type = event.get("hook_event_name", "unknown")
|
|
343
|
+
print(f"\n[{datetime.now().isoformat()}] Processing hook event: {hook_type} (PID: {os.getpid()})", file=sys.stderr)
|
|
309
344
|
|
|
310
345
|
# Increment event counter and perform periodic cleanup
|
|
311
346
|
self.events_processed += 1
|
|
@@ -320,12 +355,16 @@ class ClaudeHookHandler:
|
|
|
320
355
|
# Route event to appropriate handler
|
|
321
356
|
self._route_event(event)
|
|
322
357
|
|
|
323
|
-
# Always continue execution
|
|
324
|
-
|
|
358
|
+
# Always continue execution (only if not already sent)
|
|
359
|
+
if not _continue_sent:
|
|
360
|
+
self._continue_execution()
|
|
361
|
+
_continue_sent = True
|
|
325
362
|
|
|
326
|
-
except:
|
|
327
|
-
# Fail fast and silent
|
|
328
|
-
|
|
363
|
+
except Exception:
|
|
364
|
+
# Fail fast and silent (only send continue if not already sent)
|
|
365
|
+
if not _continue_sent:
|
|
366
|
+
self._continue_execution()
|
|
367
|
+
_continue_sent = True
|
|
329
368
|
finally:
|
|
330
369
|
# Cancel the alarm
|
|
331
370
|
signal.alarm(0)
|
|
@@ -402,6 +441,35 @@ class ClaudeHookHandler:
|
|
|
402
441
|
if DEBUG:
|
|
403
442
|
print(f"Error handling {hook_type}: {e}", file=sys.stderr)
|
|
404
443
|
|
|
444
|
+
def _get_event_key(self, event: dict) -> str:
|
|
445
|
+
"""Generate a unique key for an event to detect duplicates.
|
|
446
|
+
|
|
447
|
+
WHY: Claude Code may call the hook multiple times for the same event
|
|
448
|
+
because the hook is registered for multiple event types. We need to
|
|
449
|
+
detect and skip duplicate processing while still returning continue.
|
|
450
|
+
"""
|
|
451
|
+
# Create a key from event type, session_id, and key data
|
|
452
|
+
hook_type = event.get("hook_event_name", "unknown")
|
|
453
|
+
session_id = event.get("session_id", "")
|
|
454
|
+
|
|
455
|
+
# Add type-specific data to make the key unique
|
|
456
|
+
if hook_type == "PreToolUse":
|
|
457
|
+
tool_name = event.get("tool_name", "")
|
|
458
|
+
# For some tools, include parameters to distinguish calls
|
|
459
|
+
if tool_name == "Task":
|
|
460
|
+
tool_input = event.get("tool_input", {})
|
|
461
|
+
agent = tool_input.get("subagent_type", "")
|
|
462
|
+
prompt_preview = (tool_input.get("prompt", "") or tool_input.get("description", ""))[:50]
|
|
463
|
+
return f"{hook_type}:{session_id}:{tool_name}:{agent}:{prompt_preview}"
|
|
464
|
+
else:
|
|
465
|
+
return f"{hook_type}:{session_id}:{tool_name}"
|
|
466
|
+
elif hook_type == "UserPromptSubmit":
|
|
467
|
+
prompt_preview = event.get("prompt", "")[:50]
|
|
468
|
+
return f"{hook_type}:{session_id}:{prompt_preview}"
|
|
469
|
+
else:
|
|
470
|
+
# For other events, just use type and session
|
|
471
|
+
return f"{hook_type}:{session_id}"
|
|
472
|
+
|
|
405
473
|
def _continue_execution(self) -> None:
|
|
406
474
|
"""
|
|
407
475
|
Send continue action to Claude.
|
|
@@ -893,24 +961,26 @@ class ClaudeHookHandler:
|
|
|
893
961
|
def main():
|
|
894
962
|
"""Entry point with singleton pattern and proper cleanup."""
|
|
895
963
|
global _global_handler
|
|
964
|
+
_continue_printed = False # Track if we've already printed continue
|
|
896
965
|
|
|
897
966
|
def cleanup_handler(signum=None, frame=None):
|
|
898
967
|
"""Cleanup handler for signals and exit."""
|
|
968
|
+
nonlocal _continue_printed
|
|
899
969
|
if DEBUG:
|
|
900
970
|
print(
|
|
901
971
|
f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})",
|
|
902
972
|
file=sys.stderr,
|
|
903
973
|
)
|
|
904
|
-
#
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
974
|
+
# Only output continue if we haven't already (i.e., if interrupted by signal)
|
|
975
|
+
if signum is not None and not _continue_printed:
|
|
976
|
+
print(json.dumps({"action": "continue"}))
|
|
977
|
+
_continue_printed = True
|
|
908
978
|
sys.exit(0)
|
|
909
979
|
|
|
910
980
|
# Register cleanup handlers
|
|
911
981
|
signal.signal(signal.SIGTERM, cleanup_handler)
|
|
912
982
|
signal.signal(signal.SIGINT, cleanup_handler)
|
|
913
|
-
atexit
|
|
983
|
+
# Don't register atexit handler since we're handling exit properly in main
|
|
914
984
|
|
|
915
985
|
try:
|
|
916
986
|
# Use singleton pattern to prevent creating multiple instances
|
|
@@ -931,14 +1001,19 @@ def main():
|
|
|
931
1001
|
|
|
932
1002
|
handler = _global_handler
|
|
933
1003
|
|
|
1004
|
+
# Mark that handle() will print continue
|
|
934
1005
|
handler.handle()
|
|
1006
|
+
_continue_printed = True # Mark as printed since handle() always prints it
|
|
935
1007
|
|
|
936
|
-
#
|
|
937
|
-
|
|
1008
|
+
# handler.handle() already calls _continue_execution(), so we don't need to do it again
|
|
1009
|
+
# Just exit cleanly
|
|
1010
|
+
sys.exit(0)
|
|
938
1011
|
|
|
939
1012
|
except Exception as e:
|
|
940
|
-
#
|
|
941
|
-
|
|
1013
|
+
# Only output continue if not already printed
|
|
1014
|
+
if not _continue_printed:
|
|
1015
|
+
print(json.dumps({"action": "continue"}))
|
|
1016
|
+
_continue_printed = True
|
|
942
1017
|
# Log error for debugging
|
|
943
1018
|
if DEBUG:
|
|
944
1019
|
print(f"Hook handler error: {e}", file=sys.stderr)
|
|
@@ -57,3 +57,6 @@ if ! "$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/hoo
|
|
|
57
57
|
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Hook handler failed, see /tmp/hook-error.log" >> /tmp/hook-wrapper.log
|
|
58
58
|
exit 0
|
|
59
59
|
fi
|
|
60
|
+
|
|
61
|
+
# Success - Python handler already printed continue, just exit
|
|
62
|
+
exit 0
|
claude_mpm/init.py
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""MCP Server launcher script for Claude Desktop.
|
|
3
|
+
|
|
4
|
+
This script launches the MCP gateway server for Claude Desktop integration.
|
|
5
|
+
It handles proper Python path setup and error reporting to stderr.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
# Since we're now in src/claude_mpm/scripts/, we need to go up 3 levels to reach the project root
|
|
13
|
+
# Then down into src to add it to the path
|
|
14
|
+
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
|
15
|
+
sys.path.insert(0, os.path.join(project_root, 'src'))
|
|
16
|
+
|
|
17
|
+
def setup_logging():
|
|
18
|
+
"""Configure logging to stderr to avoid interfering with stdio protocol."""
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=logging.INFO,
|
|
21
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
22
|
+
stream=sys.stderr,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
"""Main entry point for the MCP server launcher."""
|
|
27
|
+
try:
|
|
28
|
+
# Setup logging first
|
|
29
|
+
setup_logging()
|
|
30
|
+
logger = logging.getLogger("MCPLauncher")
|
|
31
|
+
|
|
32
|
+
# Import modules after path setup
|
|
33
|
+
try:
|
|
34
|
+
from claude_mpm.services.mcp_gateway.server.stdio_server import SimpleMCPServer
|
|
35
|
+
import asyncio
|
|
36
|
+
except ImportError as e:
|
|
37
|
+
logger.error(f"Failed to import required modules: {e}")
|
|
38
|
+
logger.error("Make sure claude-mpm is properly installed")
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
|
|
41
|
+
# Create and run server
|
|
42
|
+
logger.info("Starting MCP Gateway Server...")
|
|
43
|
+
|
|
44
|
+
async def run_server():
|
|
45
|
+
"""Async function to run the server."""
|
|
46
|
+
try:
|
|
47
|
+
server = SimpleMCPServer(name="claude-mpm-gateway", version="1.0.0")
|
|
48
|
+
await server.run()
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.error(f"Server runtime error: {e}")
|
|
51
|
+
raise
|
|
52
|
+
|
|
53
|
+
# Run the async server
|
|
54
|
+
asyncio.run(run_server())
|
|
55
|
+
|
|
56
|
+
except KeyboardInterrupt:
|
|
57
|
+
# Clean shutdown on Ctrl+C
|
|
58
|
+
logger.info("Server shutdown requested")
|
|
59
|
+
sys.exit(0)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
# Log any unexpected errors
|
|
62
|
+
logger.error(f"Unexpected error: {e}")
|
|
63
|
+
import traceback
|
|
64
|
+
traceback.print_exc(file=sys.stderr)
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Wrapper Module
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
This module provides an importable entry point for the MCP wrapper script.
|
|
6
|
+
It delegates to the actual wrapper script in the scripts directory.
|
|
7
|
+
|
|
8
|
+
WHY: We need this to make the wrapper accessible as a Python module entry point
|
|
9
|
+
for the pyproject.toml scripts configuration.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def entry_point():
|
|
18
|
+
"""Entry point that delegates to the actual wrapper script."""
|
|
19
|
+
# Find the actual wrapper script
|
|
20
|
+
current_file = Path(__file__).resolve()
|
|
21
|
+
project_root = current_file.parent.parent.parent.parent
|
|
22
|
+
wrapper_script = project_root / "scripts" / "mcp_wrapper.py"
|
|
23
|
+
|
|
24
|
+
if not wrapper_script.exists():
|
|
25
|
+
print(f"Error: Wrapper script not found at {wrapper_script}", file=sys.stderr)
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
# Add the scripts directory to path and import the wrapper
|
|
29
|
+
scripts_dir = str(wrapper_script.parent)
|
|
30
|
+
if scripts_dir not in sys.path:
|
|
31
|
+
sys.path.insert(0, scripts_dir)
|
|
32
|
+
|
|
33
|
+
# Import and run the wrapper
|
|
34
|
+
import mcp_wrapper
|
|
35
|
+
mcp_wrapper.main()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
entry_point()
|