claude-mpm 4.3.6__py3-none-any.whl → 4.3.12__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 (49) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +41 -8
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +85 -43
  4. claude_mpm/agents/templates/clerk-ops.json +223 -0
  5. claude_mpm/agents/templates/data_engineer.json +41 -5
  6. claude_mpm/agents/templates/php-engineer.json +185 -0
  7. claude_mpm/agents/templates/research.json +20 -8
  8. claude_mpm/agents/templates/web_qa.json +25 -10
  9. claude_mpm/cli/__init__.py +41 -2
  10. claude_mpm/cli/commands/agents.py +2 -2
  11. claude_mpm/cli/commands/analyze.py +4 -4
  12. claude_mpm/cli/commands/cleanup.py +7 -7
  13. claude_mpm/cli/commands/configure_tui.py +2 -2
  14. claude_mpm/cli/commands/debug.py +2 -2
  15. claude_mpm/cli/commands/info.py +3 -4
  16. claude_mpm/cli/commands/mcp.py +8 -6
  17. claude_mpm/cli/commands/mcp_command_router.py +11 -0
  18. claude_mpm/cli/commands/mcp_config.py +157 -0
  19. claude_mpm/cli/commands/mcp_external_commands.py +241 -0
  20. claude_mpm/cli/commands/mcp_install_commands.py +73 -32
  21. claude_mpm/cli/commands/mcp_setup_external.py +829 -0
  22. claude_mpm/cli/commands/run.py +73 -3
  23. claude_mpm/cli/commands/search.py +285 -0
  24. claude_mpm/cli/parsers/base_parser.py +13 -0
  25. claude_mpm/cli/parsers/mcp_parser.py +17 -0
  26. claude_mpm/cli/parsers/run_parser.py +5 -0
  27. claude_mpm/cli/parsers/search_parser.py +239 -0
  28. claude_mpm/cli/startup_logging.py +20 -7
  29. claude_mpm/constants.py +1 -0
  30. claude_mpm/core/unified_agent_registry.py +7 -0
  31. claude_mpm/hooks/instruction_reinforcement.py +295 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +28 -13
  33. claude_mpm/services/agents/deployment/agent_discovery_service.py +16 -6
  34. claude_mpm/services/agents/deployment/deployment_wrapper.py +59 -0
  35. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +6 -4
  36. claude_mpm/services/cli/agent_cleanup_service.py +5 -0
  37. claude_mpm/services/mcp_config_manager.py +294 -0
  38. claude_mpm/services/mcp_gateway/config/configuration.py +17 -0
  39. claude_mpm/services/mcp_gateway/main.py +38 -0
  40. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +390 -0
  41. claude_mpm/utils/log_cleanup.py +17 -17
  42. claude_mpm/utils/subprocess_utils.py +6 -6
  43. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/METADATA +24 -1
  44. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/RECORD +48 -39
  45. claude_mpm/agents/templates/agent-manager.md +0 -619
  46. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/WHEEL +0 -0
  47. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/entry_points.txt +0 -0
  48. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/licenses/LICENSE +0 -0
  49. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,390 @@
1
+ """
2
+ External MCP Services Integration
3
+ ==================================
4
+
5
+ Manages installation and basic setup of external MCP services like mcp-vector-search
6
+ and mcp-browser. These services run as separate MCP servers in Claude Desktop,
7
+ not as part of the Claude MPM MCP Gateway.
8
+
9
+ Note: As of the latest architecture, external services are registered as separate
10
+ MCP servers in Claude Desktop configuration, not as tools within the gateway.
11
+ """
12
+
13
+ import asyncio
14
+ import json
15
+ import subprocess
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from claude_mpm.services.mcp_gateway.core.base import BaseMCPService
21
+ from claude_mpm.services.mcp_gateway.tools.base_adapter import BaseMCPToolAdapter
22
+
23
+
24
+ class ExternalMCPService(BaseMCPToolAdapter):
25
+ """Base class for external MCP service integration."""
26
+
27
+ def __init__(self, service_name: str, package_name: str):
28
+ """
29
+ Initialize external MCP service.
30
+
31
+ Args:
32
+ service_name: Name of the service for MCP
33
+ package_name: Python package name to install/run
34
+ """
35
+ super().__init__()
36
+ self.service_name = service_name
37
+ self.package_name = package_name
38
+ self.process = None
39
+ self._is_installed = False
40
+
41
+ async def initialize(self) -> bool:
42
+ """Initialize the external service."""
43
+ try:
44
+ # Check if package is installed
45
+ self._is_installed = await self._check_installation()
46
+
47
+ if not self._is_installed:
48
+ self.logger.warning(f"{self.package_name} not installed, attempting installation...")
49
+ await self._install_package()
50
+ self._is_installed = await self._check_installation()
51
+
52
+ if not self._is_installed:
53
+ self.logger.error(f"Failed to install {self.package_name}")
54
+ return False
55
+
56
+ self.logger.info(f"{self.package_name} is available")
57
+ return True
58
+
59
+ except Exception as e:
60
+ self.logger.error(f"Failed to initialize {self.service_name}: {e}")
61
+ return False
62
+
63
+ async def _check_installation(self) -> bool:
64
+ """Check if the package is installed."""
65
+ try:
66
+ result = subprocess.run(
67
+ [sys.executable, "-m", self.package_name.replace("-", "_"), "--help"],
68
+ capture_output=True,
69
+ text=True,
70
+ timeout=5
71
+ )
72
+ return result.returncode == 0
73
+ except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.CalledProcessError):
74
+ return False
75
+
76
+ async def _install_package(self) -> bool:
77
+ """Install the package using pip."""
78
+ try:
79
+ self.logger.info(f"Installing {self.package_name}...")
80
+ result = subprocess.run(
81
+ [sys.executable, "-m", "pip", "install", self.package_name],
82
+ capture_output=True,
83
+ text=True,
84
+ timeout=30
85
+ )
86
+
87
+ if result.returncode == 0:
88
+ self.logger.info(f"Successfully installed {self.package_name}")
89
+ return True
90
+ else:
91
+ self.logger.error(f"Failed to install {self.package_name}: {result.stderr}")
92
+ return False
93
+
94
+ except Exception as e:
95
+ self.logger.error(f"Error installing {self.package_name}: {e}")
96
+ return False
97
+
98
+ def get_definition(self) -> Dict[str, Any]:
99
+ """Get service definition for MCP registration."""
100
+ return {
101
+ "name": self.service_name,
102
+ "description": f"External MCP service: {self.package_name}",
103
+ "type": "external_service",
104
+ "package": self.package_name,
105
+ "installed": self._is_installed
106
+ }
107
+
108
+
109
+ class MCPVectorSearchService(ExternalMCPService):
110
+ """MCP Vector Search service integration."""
111
+
112
+ def __init__(self):
113
+ """Initialize MCP Vector Search service."""
114
+ super().__init__("mcp-vector-search", "mcp-vector-search")
115
+
116
+ def get_definition(self) -> Dict[str, Any]:
117
+ """Get tool definition for MCP registration."""
118
+ base_def = super().get_definition()
119
+ base_def.update({
120
+ "description": "Semantic code search powered by vector embeddings",
121
+ "tools": [
122
+ {
123
+ "name": "mcp__mcp-vector-search__search_code",
124
+ "description": "Search for code using semantic similarity",
125
+ "inputSchema": {
126
+ "type": "object",
127
+ "properties": {
128
+ "query": {"type": "string", "description": "The search query"},
129
+ "limit": {"type": "integer", "default": 10},
130
+ "similarity_threshold": {"type": "number", "default": 0.3},
131
+ "language": {"type": "string"},
132
+ "file_extensions": {"type": "array", "items": {"type": "string"}},
133
+ "files": {"type": "string"},
134
+ "class_name": {"type": "string"},
135
+ "function_name": {"type": "string"}
136
+ },
137
+ "required": ["query"]
138
+ }
139
+ },
140
+ {
141
+ "name": "mcp__mcp-vector-search__search_similar",
142
+ "description": "Find code similar to a specific file or function",
143
+ "inputSchema": {
144
+ "type": "object",
145
+ "properties": {
146
+ "file_path": {"type": "string", "description": "Path to the file"},
147
+ "function_name": {"type": "string"},
148
+ "limit": {"type": "integer", "default": 10},
149
+ "similarity_threshold": {"type": "number", "default": 0.3}
150
+ },
151
+ "required": ["file_path"]
152
+ }
153
+ },
154
+ {
155
+ "name": "mcp__mcp-vector-search__search_context",
156
+ "description": "Search for code based on contextual description",
157
+ "inputSchema": {
158
+ "type": "object",
159
+ "properties": {
160
+ "description": {"type": "string", "description": "Contextual description"},
161
+ "focus_areas": {"type": "array", "items": {"type": "string"}},
162
+ "limit": {"type": "integer", "default": 10}
163
+ },
164
+ "required": ["description"]
165
+ }
166
+ },
167
+ {
168
+ "name": "mcp__mcp-vector-search__get_project_status",
169
+ "description": "Get project indexing status and statistics",
170
+ "inputSchema": {
171
+ "type": "object",
172
+ "properties": {},
173
+ "required": []
174
+ }
175
+ },
176
+ {
177
+ "name": "mcp__mcp-vector-search__index_project",
178
+ "description": "Index or reindex the project codebase",
179
+ "inputSchema": {
180
+ "type": "object",
181
+ "properties": {
182
+ "force": {"type": "boolean", "default": False},
183
+ "file_extensions": {"type": "array", "items": {"type": "string"}}
184
+ },
185
+ "required": []
186
+ }
187
+ }
188
+ ]
189
+ })
190
+ return base_def
191
+
192
+ async def invoke(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
193
+ """Invoke a tool from mcp-vector-search."""
194
+ try:
195
+ # Extract the actual tool name (remove prefix)
196
+ actual_tool = tool_name.replace("mcp__mcp-vector-search__", "")
197
+
198
+ # Prepare the command
199
+ cmd = [
200
+ sys.executable, "-m", "mcp_vector_search",
201
+ "--tool", actual_tool,
202
+ "--args", json.dumps(arguments)
203
+ ]
204
+
205
+ # Run the command
206
+ result = subprocess.run(
207
+ cmd,
208
+ capture_output=True,
209
+ text=True,
210
+ timeout=30,
211
+ cwd=Path.cwd() # Use current working directory for project context
212
+ )
213
+
214
+ if result.returncode == 0:
215
+ try:
216
+ return json.loads(result.stdout)
217
+ except json.JSONDecodeError:
218
+ return {"result": result.stdout}
219
+ else:
220
+ return {"error": result.stderr or "Tool invocation failed"}
221
+
222
+ except subprocess.TimeoutExpired:
223
+ return {"error": "Tool invocation timed out"}
224
+ except Exception as e:
225
+ return {"error": str(e)}
226
+
227
+
228
+ class MCPBrowserService(ExternalMCPService):
229
+ """MCP Browser service integration."""
230
+
231
+ def __init__(self):
232
+ """Initialize MCP Browser service."""
233
+ super().__init__("mcp-browser", "mcp-browser")
234
+
235
+ def get_definition(self) -> Dict[str, Any]:
236
+ """Get tool definition for MCP registration."""
237
+ base_def = super().get_definition()
238
+ base_def.update({
239
+ "description": "Web browsing and content extraction capabilities",
240
+ "tools": [
241
+ {
242
+ "name": "mcp__mcp-browser__browse",
243
+ "description": "Browse a webpage and extract content",
244
+ "inputSchema": {
245
+ "type": "object",
246
+ "properties": {
247
+ "url": {"type": "string", "description": "URL to browse"},
248
+ "extract": {"type": "string", "description": "What to extract"}
249
+ },
250
+ "required": ["url"]
251
+ }
252
+ },
253
+ {
254
+ "name": "mcp__mcp-browser__search",
255
+ "description": "Search the web",
256
+ "inputSchema": {
257
+ "type": "object",
258
+ "properties": {
259
+ "query": {"type": "string", "description": "Search query"},
260
+ "num_results": {"type": "integer", "default": 10}
261
+ },
262
+ "required": ["query"]
263
+ }
264
+ },
265
+ {
266
+ "name": "mcp__mcp-browser__screenshot",
267
+ "description": "Take a screenshot of a webpage",
268
+ "inputSchema": {
269
+ "type": "object",
270
+ "properties": {
271
+ "url": {"type": "string", "description": "URL to screenshot"},
272
+ "full_page": {"type": "boolean", "default": False}
273
+ },
274
+ "required": ["url"]
275
+ }
276
+ }
277
+ ]
278
+ })
279
+ return base_def
280
+
281
+ async def invoke(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
282
+ """Invoke a tool from mcp-browser."""
283
+ try:
284
+ # Extract the actual tool name (remove prefix)
285
+ actual_tool = tool_name.replace("mcp__mcp-browser__", "")
286
+
287
+ # Prepare the command
288
+ cmd = [
289
+ sys.executable, "-m", "mcp_browser",
290
+ "--tool", actual_tool,
291
+ "--args", json.dumps(arguments)
292
+ ]
293
+
294
+ # Run the command
295
+ result = subprocess.run(
296
+ cmd,
297
+ capture_output=True,
298
+ text=True,
299
+ timeout=30
300
+ )
301
+
302
+ if result.returncode == 0:
303
+ try:
304
+ return json.loads(result.stdout)
305
+ except json.JSONDecodeError:
306
+ return {"result": result.stdout}
307
+ else:
308
+ return {"error": result.stderr or "Tool invocation failed"}
309
+
310
+ except subprocess.TimeoutExpired:
311
+ return {"error": "Tool invocation timed out"}
312
+ except Exception as e:
313
+ return {"error": str(e)}
314
+
315
+
316
+ class ExternalMCPServiceManager:
317
+ """Manager for external MCP services.
318
+
319
+ This manager is responsible for checking and installing Python packages
320
+ for external MCP services. The actual registration of these services
321
+ happens in Claude Desktop configuration as separate MCP servers.
322
+
323
+ Note: This class is maintained for backward compatibility and package
324
+ management. The actual tool registration is handled by separate MCP
325
+ server instances in Claude Desktop.
326
+ """
327
+
328
+ def __init__(self):
329
+ """Initialize the service manager."""
330
+ self.services: List[ExternalMCPService] = []
331
+ self.logger = None
332
+
333
+ async def initialize_services(self) -> List[ExternalMCPService]:
334
+ """Initialize all external MCP services.
335
+
336
+ This method checks if external service packages are installed
337
+ and attempts to install them if missing. It does NOT register
338
+ them as tools in the gateway - they run as separate MCP servers.
339
+ """
340
+ # Create service instances
341
+ services = [
342
+ MCPVectorSearchService(),
343
+ MCPBrowserService()
344
+ ]
345
+
346
+ # Initialize each service
347
+ initialized_services = []
348
+ for service in services:
349
+ try:
350
+ if await service.initialize():
351
+ initialized_services.append(service)
352
+ if self.logger:
353
+ self.logger.info(f"Initialized external service: {service.service_name}")
354
+ else:
355
+ if self.logger:
356
+ self.logger.warning(f"Failed to initialize: {service.service_name}")
357
+ except Exception as e:
358
+ if self.logger:
359
+ self.logger.error(f"Error initializing {service.service_name}: {e}")
360
+
361
+ self.services = initialized_services
362
+ return initialized_services
363
+
364
+ def get_all_tools(self) -> List[Dict[str, Any]]:
365
+ """Get all tool definitions from external services."""
366
+ all_tools = []
367
+ for service in self.services:
368
+ service_def = service.get_definition()
369
+ if "tools" in service_def:
370
+ all_tools.extend(service_def["tools"])
371
+ return all_tools
372
+
373
+ async def invoke_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
374
+ """Invoke a tool from any registered external service."""
375
+ # Find the service that handles this tool
376
+ for service in self.services:
377
+ if tool_name.startswith(f"mcp__{service.service_name}__"):
378
+ if isinstance(service, (MCPVectorSearchService, MCPBrowserService)):
379
+ return await service.invoke(tool_name, arguments)
380
+
381
+ return {"error": f"No service found for tool: {tool_name}"}
382
+
383
+ async def shutdown(self):
384
+ """Shutdown all external services."""
385
+ for service in self.services:
386
+ try:
387
+ await service.shutdown()
388
+ except Exception as e:
389
+ if self.logger:
390
+ self.logger.warning(f"Error shutting down {service.service_name}: {e}")
@@ -9,7 +9,7 @@ import gzip
9
9
  import logging
10
10
  import os
11
11
  import shutil
12
- from datetime import datetime, timedelta
12
+ from datetime import datetime, timedelta, timezone
13
13
  from pathlib import Path
14
14
  from typing import Dict, Optional, Tuple
15
15
 
@@ -92,7 +92,7 @@ class LogCleanupUtility:
92
92
  logger.info(f"Sessions directory not found: {sessions_dir}")
93
93
  return 0, 0.0
94
94
 
95
- cutoff_time = datetime.now() - timedelta(days=max_age_days)
95
+ cutoff_time = datetime.now(timezone.utc) - timedelta(days=max_age_days)
96
96
  removed_count = 0
97
97
  total_size = 0.0
98
98
 
@@ -107,7 +107,7 @@ class LogCleanupUtility:
107
107
 
108
108
  try:
109
109
  # Check directory modification time
110
- mtime = datetime.fromtimestamp(session_dir.stat().st_mtime)
110
+ mtime = datetime.fromtimestamp(session_dir.stat().st_mtime, tz=timezone.utc)
111
111
 
112
112
  if mtime < cutoff_time:
113
113
  # Calculate directory size
@@ -117,14 +117,14 @@ class LogCleanupUtility:
117
117
  if dry_run:
118
118
  logger.info(
119
119
  f"[DRY RUN] Would remove session: {session_dir.name} "
120
- f"(age: {(datetime.now() - mtime).days} days, "
120
+ f"(age: {(datetime.now(timezone.utc) - mtime).days} days, "
121
121
  f"size: {dir_size:.2f} MB)"
122
122
  )
123
123
  else:
124
124
  shutil.rmtree(session_dir)
125
125
  logger.info(
126
126
  f"Removed session: {session_dir.name} "
127
- f"(age: {(datetime.now() - mtime).days} days, "
127
+ f"(age: {(datetime.now(timezone.utc) - mtime).days} days, "
128
128
  f"size: {dir_size:.2f} MB)"
129
129
  )
130
130
 
@@ -159,7 +159,7 @@ class LogCleanupUtility:
159
159
  Returns:
160
160
  Tuple of (files removed, space freed in MB)
161
161
  """
162
- cutoff_time = datetime.now() - timedelta(days=max_age_days)
162
+ cutoff_time = datetime.now(timezone.utc) - timedelta(days=max_age_days)
163
163
  removed_count = 0
164
164
  total_size = 0.0
165
165
 
@@ -169,7 +169,7 @@ class LogCleanupUtility:
169
169
  for ext in LogCleanupConfig.ARCHIVE_EXTENSIONS:
170
170
  for archive_file in self.base_log_dir.rglob(f"*{ext}"):
171
171
  try:
172
- mtime = datetime.fromtimestamp(archive_file.stat().st_mtime)
172
+ mtime = datetime.fromtimestamp(archive_file.stat().st_mtime, tz=timezone.utc)
173
173
 
174
174
  if mtime < cutoff_time:
175
175
  file_size = archive_file.stat().st_size / (1024 * 1024) # MB
@@ -178,14 +178,14 @@ class LogCleanupUtility:
178
178
  if dry_run:
179
179
  logger.info(
180
180
  f"[DRY RUN] Would remove archive: {archive_file.name} "
181
- f"(age: {(datetime.now() - mtime).days} days, "
181
+ f"(age: {(datetime.now(timezone.utc) - mtime).days} days, "
182
182
  f"size: {file_size:.2f} MB)"
183
183
  )
184
184
  else:
185
185
  archive_file.unlink()
186
186
  logger.info(
187
187
  f"Removed archive: {archive_file.name} "
188
- f"(age: {(datetime.now() - mtime).days} days, "
188
+ f"(age: {(datetime.now(timezone.utc) - mtime).days} days, "
189
189
  f"size: {file_size:.2f} MB)"
190
190
  )
191
191
 
@@ -218,7 +218,7 @@ class LogCleanupUtility:
218
218
  Returns:
219
219
  Tuple of (files removed, space freed in MB)
220
220
  """
221
- cutoff_time = datetime.now() - timedelta(days=max_age_days)
221
+ cutoff_time = datetime.now(timezone.utc) - timedelta(days=max_age_days)
222
222
  removed_count = 0
223
223
  total_size = 0.0
224
224
 
@@ -238,7 +238,7 @@ class LogCleanupUtility:
238
238
 
239
239
  for log_file in log_dir.glob(pattern):
240
240
  try:
241
- mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
241
+ mtime = datetime.fromtimestamp(log_file.stat().st_mtime, tz=timezone.utc)
242
242
 
243
243
  if mtime < cutoff_time:
244
244
  file_size = log_file.stat().st_size / (1024 * 1024) # MB
@@ -247,14 +247,14 @@ class LogCleanupUtility:
247
247
  if dry_run:
248
248
  logger.info(
249
249
  f"[DRY RUN] Would remove log: {log_file.name} "
250
- f"(age: {(datetime.now() - mtime).days} days, "
250
+ f"(age: {(datetime.now(timezone.utc) - mtime).days} days, "
251
251
  f"size: {file_size:.2f} MB)"
252
252
  )
253
253
  else:
254
254
  log_file.unlink()
255
255
  logger.info(
256
256
  f"Removed log: {log_file.name} "
257
- f"(age: {(datetime.now() - mtime).days} days, "
257
+ f"(age: {(datetime.now(timezone.utc) - mtime).days} days, "
258
258
  f"size: {file_size:.2f} MB)"
259
259
  )
260
260
 
@@ -321,7 +321,7 @@ class LogCleanupUtility:
321
321
  Returns:
322
322
  Tuple of (files compressed, space saved in MB)
323
323
  """
324
- cutoff_time = datetime.now() - timedelta(days=age_days)
324
+ cutoff_time = datetime.now(timezone.utc) - timedelta(days=age_days)
325
325
  compressed_count = 0
326
326
  space_saved = 0.0
327
327
 
@@ -331,7 +331,7 @@ class LogCleanupUtility:
331
331
  continue
332
332
 
333
333
  try:
334
- mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
334
+ mtime = datetime.fromtimestamp(log_file.stat().st_mtime, tz=timezone.utc)
335
335
 
336
336
  if mtime < cutoff_time:
337
337
  original_size = log_file.stat().st_size / (1024 * 1024) # MB
@@ -408,7 +408,7 @@ class LogCleanupUtility:
408
408
  stats["oldest_session"] = {
409
409
  "name": oldest.name,
410
410
  "age_days": (
411
- datetime.now() - datetime.fromtimestamp(oldest.stat().st_mtime)
411
+ datetime.now(timezone.utc) - datetime.fromtimestamp(oldest.stat().st_mtime, tz=timezone.utc)
412
412
  ).days,
413
413
  }
414
414
 
@@ -429,7 +429,7 @@ class LogCleanupUtility:
429
429
  "name": oldest_log.name,
430
430
  "path": str(oldest_log.relative_to(self.base_log_dir)),
431
431
  "age_days": (
432
- datetime.now() - datetime.fromtimestamp(oldest_log.stat().st_mtime)
432
+ datetime.now(timezone.utc) - datetime.fromtimestamp(oldest_log.stat().st_mtime, tz=timezone.utc)
433
433
  ).days,
434
434
  }
435
435
 
@@ -83,7 +83,7 @@ def run_command(command_string: str, timeout: float = 60) -> str:
83
83
  returncode=getattr(e, "returncode", None),
84
84
  stdout=getattr(e, "stdout", ""),
85
85
  stderr=stderr,
86
- )
86
+ ) from e
87
87
 
88
88
 
89
89
  def run_subprocess(
@@ -137,16 +137,16 @@ def run_subprocess(
137
137
  returncode=None,
138
138
  stdout=e.stdout.decode() if e.stdout else "",
139
139
  stderr=e.stderr.decode() if e.stderr else "",
140
- )
140
+ ) from e
141
141
  except subprocess.CalledProcessError as e:
142
142
  raise SubprocessError(
143
143
  f"Command failed with return code {e.returncode}: {' '.join(cmd)}",
144
144
  returncode=e.returncode,
145
145
  stdout=e.stdout if e.stdout else "",
146
146
  stderr=e.stderr if e.stderr else "",
147
- )
147
+ ) from e
148
148
  except Exception as e:
149
- raise SubprocessError(f"Subprocess execution failed: {e}")
149
+ raise SubprocessError(f"Subprocess execution failed: {e}") from e
150
150
 
151
151
 
152
152
  async def run_subprocess_async(
@@ -195,7 +195,7 @@ async def run_subprocess_async(
195
195
  await process.wait()
196
196
  raise SubprocessError(
197
197
  f"Command timed out after {timeout}s: {' '.join(cmd)}", returncode=None
198
- )
198
+ ) from None
199
199
 
200
200
  return SubprocessResult(
201
201
  returncode=process.returncode,
@@ -205,7 +205,7 @@ async def run_subprocess_async(
205
205
 
206
206
  except Exception as e:
207
207
  if not isinstance(e, SubprocessError):
208
- raise SubprocessError(f"Async subprocess execution failed: {e}")
208
+ raise SubprocessError(f"Async subprocess execution failed: {e}") from e
209
209
  raise
210
210
 
211
211
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.3.6
3
+ Version: 4.3.12
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team
@@ -42,6 +42,9 @@ Requires-Dist: mistune>=3.0.0
42
42
  Requires-Dist: tree-sitter>=0.21.0
43
43
  Requires-Dist: ijson>=3.2.0
44
44
  Requires-Dist: mcp>=0.1.0
45
+ Requires-Dist: mcp-vector-search>=0.1.0
46
+ Requires-Dist: mcp-browser>=0.1.0
47
+ Requires-Dist: mcp-ticketer>=0.1.0
45
48
  Requires-Dist: toml>=0.10.2
46
49
  Requires-Dist: packaging>=21.0
47
50
  Requires-Dist: pydantic>=2.0.0
@@ -120,6 +123,26 @@ Requires-Dist: tree-sitter-cpp>=0.21.0; extra == "agents"
120
123
  Requires-Dist: tree-sitter-c>=0.21.0; extra == "agents"
121
124
  Requires-Dist: tree-sitter-ruby>=0.21.0; extra == "agents"
122
125
  Requires-Dist: tree-sitter-php>=0.21.0; extra == "agents"
126
+ Provides-Extra: data-processing
127
+ Requires-Dist: pandas>=2.1.0; extra == "data-processing"
128
+ Requires-Dist: openpyxl>=3.1.0; extra == "data-processing"
129
+ Requires-Dist: xlsxwriter>=3.1.0; extra == "data-processing"
130
+ Requires-Dist: numpy>=1.24.0; extra == "data-processing"
131
+ Requires-Dist: pyarrow>=14.0.0; extra == "data-processing"
132
+ Requires-Dist: dask>=2023.12.0; extra == "data-processing"
133
+ Requires-Dist: polars>=0.19.0; extra == "data-processing"
134
+ Requires-Dist: xlrd>=2.0.0; extra == "data-processing"
135
+ Requires-Dist: xlwt>=1.3.0; extra == "data-processing"
136
+ Requires-Dist: csvkit>=1.3.0; extra == "data-processing"
137
+ Requires-Dist: tabulate>=0.9.0; extra == "data-processing"
138
+ Requires-Dist: python-dateutil>=2.8.0; extra == "data-processing"
139
+ Requires-Dist: lxml>=4.9.0; extra == "data-processing"
140
+ Requires-Dist: sqlalchemy>=2.0.0; extra == "data-processing"
141
+ Requires-Dist: psycopg2-binary>=2.9.0; extra == "data-processing"
142
+ Requires-Dist: pymongo>=4.5.0; extra == "data-processing"
143
+ Requires-Dist: redis>=5.0.0; extra == "data-processing"
144
+ Requires-Dist: beautifulsoup4>=4.12.0; extra == "data-processing"
145
+ Requires-Dist: jsonschema>=4.19.0; extra == "data-processing"
123
146
  Dynamic: license-file
124
147
 
125
148
  # Claude MPM - Multi-Agent Project Manager