claude-mpm 4.3.11__py3-none-any.whl → 4.3.13__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 (207) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +390 -28
  3. claude_mpm/agents/templates/data_engineer.json +39 -14
  4. claude_mpm/agents/templates/research.json +20 -8
  5. claude_mpm/agents/templates/web_qa.json +25 -10
  6. claude_mpm/cli/__init__.py +1 -0
  7. claude_mpm/cli/commands/agent_manager.py +3 -3
  8. claude_mpm/cli/commands/agents.py +2 -2
  9. claude_mpm/cli/commands/aggregate.py +1 -1
  10. claude_mpm/cli/commands/config.py +2 -2
  11. claude_mpm/cli/commands/configure.py +5 -5
  12. claude_mpm/cli/commands/configure_tui.py +7 -7
  13. claude_mpm/cli/commands/dashboard.py +1 -1
  14. claude_mpm/cli/commands/debug.py +5 -5
  15. claude_mpm/cli/commands/mcp.py +1 -1
  16. claude_mpm/cli/commands/mcp_command_router.py +12 -1
  17. claude_mpm/cli/commands/mcp_config.py +154 -0
  18. claude_mpm/cli/commands/mcp_external_commands.py +249 -0
  19. claude_mpm/cli/commands/mcp_install_commands.py +93 -24
  20. claude_mpm/cli/commands/mcp_setup_external.py +870 -0
  21. claude_mpm/cli/commands/monitor.py +2 -2
  22. claude_mpm/cli/commands/mpm_init_handler.py +1 -1
  23. claude_mpm/cli/commands/run.py +114 -0
  24. claude_mpm/cli/commands/search.py +292 -0
  25. claude_mpm/cli/interactive/agent_wizard.py +2 -2
  26. claude_mpm/cli/parsers/base_parser.py +13 -0
  27. claude_mpm/cli/parsers/mcp_parser.py +15 -0
  28. claude_mpm/cli/parsers/run_parser.py +5 -0
  29. claude_mpm/cli/parsers/search_parser.py +245 -0
  30. claude_mpm/cli/startup_logging.py +3 -5
  31. claude_mpm/cli/utils.py +1 -1
  32. claude_mpm/constants.py +1 -0
  33. claude_mpm/core/agent_registry.py +12 -8
  34. claude_mpm/core/agent_session_manager.py +8 -8
  35. claude_mpm/core/api_validator.py +4 -4
  36. claude_mpm/core/base_service.py +10 -10
  37. claude_mpm/core/cache.py +5 -5
  38. claude_mpm/core/config_constants.py +1 -1
  39. claude_mpm/core/container.py +1 -1
  40. claude_mpm/core/error_handler.py +2 -2
  41. claude_mpm/core/file_utils.py +1 -1
  42. claude_mpm/core/framework_loader.py +3 -3
  43. claude_mpm/core/hook_manager.py +8 -6
  44. claude_mpm/core/instruction_reinforcement_hook.py +2 -2
  45. claude_mpm/core/interactive_session.py +1 -1
  46. claude_mpm/core/lazy.py +3 -3
  47. claude_mpm/core/log_manager.py +16 -12
  48. claude_mpm/core/logger.py +16 -11
  49. claude_mpm/core/logging_config.py +4 -2
  50. claude_mpm/core/oneshot_session.py +1 -1
  51. claude_mpm/core/optimized_agent_loader.py +6 -6
  52. claude_mpm/core/output_style_manager.py +1 -1
  53. claude_mpm/core/pm_hook_interceptor.py +3 -3
  54. claude_mpm/core/service_registry.py +1 -1
  55. claude_mpm/core/session_manager.py +11 -9
  56. claude_mpm/core/socketio_pool.py +13 -13
  57. claude_mpm/core/types.py +2 -2
  58. claude_mpm/core/unified_agent_registry.py +9 -2
  59. claude_mpm/core/unified_paths.py +1 -1
  60. claude_mpm/dashboard/analysis_runner.py +4 -4
  61. claude_mpm/dashboard/api/simple_directory.py +1 -1
  62. claude_mpm/generators/agent_profile_generator.py +4 -2
  63. claude_mpm/hooks/base_hook.py +2 -2
  64. claude_mpm/hooks/claude_hooks/connection_pool.py +4 -4
  65. claude_mpm/hooks/claude_hooks/event_handlers.py +12 -12
  66. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -4
  67. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +3 -3
  68. claude_mpm/hooks/claude_hooks/hook_handler_original.py +15 -14
  69. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +4 -4
  70. claude_mpm/hooks/claude_hooks/installer.py +3 -3
  71. claude_mpm/hooks/claude_hooks/memory_integration.py +3 -3
  72. claude_mpm/hooks/claude_hooks/response_tracking.py +3 -3
  73. claude_mpm/hooks/claude_hooks/services/connection_manager.py +5 -5
  74. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +3 -3
  75. claude_mpm/hooks/claude_hooks/services/state_manager.py +8 -7
  76. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +3 -3
  77. claude_mpm/hooks/claude_hooks/tool_analysis.py +2 -2
  78. claude_mpm/hooks/memory_integration_hook.py +1 -1
  79. claude_mpm/hooks/tool_call_interceptor.py +2 -2
  80. claude_mpm/models/agent_session.py +5 -5
  81. claude_mpm/services/__init__.py +1 -1
  82. claude_mpm/services/agent_capabilities_service.py +1 -1
  83. claude_mpm/services/agents/agent_builder.py +3 -3
  84. claude_mpm/services/agents/deployment/agent_deployment.py +29 -13
  85. claude_mpm/services/agents/deployment/agent_discovery_service.py +22 -6
  86. claude_mpm/services/agents/deployment/agent_filesystem_manager.py +7 -5
  87. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +3 -1
  88. claude_mpm/services/agents/deployment/agent_metrics_collector.py +1 -1
  89. claude_mpm/services/agents/deployment/agent_operation_service.py +2 -2
  90. claude_mpm/services/agents/deployment/agent_state_service.py +2 -2
  91. claude_mpm/services/agents/deployment/agent_template_builder.py +1 -1
  92. claude_mpm/services/agents/deployment/agent_versioning.py +1 -1
  93. claude_mpm/services/agents/deployment/deployment_wrapper.py +2 -3
  94. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +6 -4
  95. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +1 -1
  96. claude_mpm/services/agents/loading/agent_profile_loader.py +5 -3
  97. claude_mpm/services/agents/loading/base_agent_manager.py +2 -2
  98. claude_mpm/services/agents/local_template_manager.py +6 -6
  99. claude_mpm/services/agents/management/agent_management_service.py +3 -3
  100. claude_mpm/services/agents/memory/content_manager.py +3 -3
  101. claude_mpm/services/agents/memory/memory_format_service.py +2 -2
  102. claude_mpm/services/agents/memory/template_generator.py +3 -3
  103. claude_mpm/services/agents/registry/__init__.py +1 -1
  104. claude_mpm/services/agents/registry/modification_tracker.py +2 -2
  105. claude_mpm/services/async_session_logger.py +3 -3
  106. claude_mpm/services/claude_session_logger.py +4 -4
  107. claude_mpm/services/cli/agent_cleanup_service.py +5 -0
  108. claude_mpm/services/cli/agent_listing_service.py +1 -1
  109. claude_mpm/services/cli/agent_validation_service.py +1 -0
  110. claude_mpm/services/cli/memory_crud_service.py +11 -6
  111. claude_mpm/services/cli/memory_output_formatter.py +1 -1
  112. claude_mpm/services/cli/session_manager.py +15 -11
  113. claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
  114. claude_mpm/services/core/memory_manager.py +81 -23
  115. claude_mpm/services/core/path_resolver.py +2 -2
  116. claude_mpm/services/diagnostics/checks/installation_check.py +1 -1
  117. claude_mpm/services/event_aggregator.py +4 -2
  118. claude_mpm/services/event_bus/direct_relay.py +5 -3
  119. claude_mpm/services/event_bus/event_bus.py +3 -3
  120. claude_mpm/services/event_bus/relay.py +6 -4
  121. claude_mpm/services/events/consumers/dead_letter.py +5 -3
  122. claude_mpm/services/events/core.py +3 -3
  123. claude_mpm/services/events/producers/hook.py +6 -6
  124. claude_mpm/services/events/producers/system.py +8 -8
  125. claude_mpm/services/exceptions.py +5 -5
  126. claude_mpm/services/framework_claude_md_generator/content_assembler.py +3 -3
  127. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +2 -2
  128. claude_mpm/services/hook_installer_service.py +1 -1
  129. claude_mpm/services/infrastructure/context_preservation.py +6 -4
  130. claude_mpm/services/infrastructure/daemon_manager.py +2 -2
  131. claude_mpm/services/infrastructure/logging.py +2 -2
  132. claude_mpm/services/mcp_config_manager.py +439 -0
  133. claude_mpm/services/mcp_gateway/__init__.py +1 -1
  134. claude_mpm/services/mcp_gateway/auto_configure.py +3 -3
  135. claude_mpm/services/mcp_gateway/config/config_loader.py +1 -1
  136. claude_mpm/services/mcp_gateway/config/configuration.py +18 -1
  137. claude_mpm/services/mcp_gateway/core/base.py +2 -2
  138. claude_mpm/services/mcp_gateway/main.py +52 -0
  139. claude_mpm/services/mcp_gateway/registry/tool_registry.py +10 -8
  140. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +4 -4
  141. claude_mpm/services/mcp_gateway/server/stdio_handler.py +1 -1
  142. claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -3
  143. claude_mpm/services/mcp_gateway/tools/base_adapter.py +15 -15
  144. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +7 -5
  145. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +443 -0
  146. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +5 -5
  147. claude_mpm/services/mcp_gateway/tools/hello_world.py +9 -9
  148. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +16 -16
  149. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +17 -17
  150. claude_mpm/services/memory/builder.py +7 -5
  151. claude_mpm/services/memory/indexed_memory.py +4 -4
  152. claude_mpm/services/memory/optimizer.py +6 -6
  153. claude_mpm/services/memory/router.py +3 -3
  154. claude_mpm/services/monitor/daemon.py +1 -1
  155. claude_mpm/services/monitor/daemon_manager.py +6 -6
  156. claude_mpm/services/monitor/event_emitter.py +2 -2
  157. claude_mpm/services/monitor/handlers/file.py +1 -1
  158. claude_mpm/services/monitor/management/lifecycle.py +1 -1
  159. claude_mpm/services/monitor/server.py +4 -4
  160. claude_mpm/services/monitor_build_service.py +2 -2
  161. claude_mpm/services/port_manager.py +2 -2
  162. claude_mpm/services/response_tracker.py +2 -2
  163. claude_mpm/services/session_management_service.py +3 -2
  164. claude_mpm/services/socketio/client_proxy.py +2 -2
  165. claude_mpm/services/socketio/dashboard_server.py +4 -3
  166. claude_mpm/services/socketio/event_normalizer.py +12 -8
  167. claude_mpm/services/socketio/handlers/base.py +2 -2
  168. claude_mpm/services/socketio/handlers/connection.py +10 -10
  169. claude_mpm/services/socketio/handlers/connection_handler.py +13 -10
  170. claude_mpm/services/socketio/handlers/file.py +1 -1
  171. claude_mpm/services/socketio/handlers/git.py +1 -1
  172. claude_mpm/services/socketio/handlers/hook.py +16 -15
  173. claude_mpm/services/socketio/migration_utils.py +1 -1
  174. claude_mpm/services/socketio/monitor_client.py +5 -5
  175. claude_mpm/services/socketio/server/broadcaster.py +9 -7
  176. claude_mpm/services/socketio/server/connection_manager.py +2 -2
  177. claude_mpm/services/socketio/server/core.py +7 -5
  178. claude_mpm/services/socketio/server/eventbus_integration.py +18 -11
  179. claude_mpm/services/socketio/server/main.py +13 -13
  180. claude_mpm/services/socketio_client_manager.py +4 -4
  181. claude_mpm/services/system_instructions_service.py +2 -2
  182. claude_mpm/services/ticket_services/validation_service.py +1 -1
  183. claude_mpm/services/utility_service.py +5 -2
  184. claude_mpm/services/version_control/branch_strategy.py +2 -2
  185. claude_mpm/services/version_control/git_operations.py +22 -20
  186. claude_mpm/services/version_control/semantic_versioning.py +3 -3
  187. claude_mpm/services/version_control/version_parser.py +7 -5
  188. claude_mpm/services/visualization/mermaid_generator.py +1 -1
  189. claude_mpm/storage/state_storage.py +1 -1
  190. claude_mpm/tools/code_tree_analyzer.py +19 -18
  191. claude_mpm/tools/code_tree_builder.py +2 -2
  192. claude_mpm/tools/code_tree_events.py +10 -8
  193. claude_mpm/tools/socketio_debug.py +3 -3
  194. claude_mpm/utils/agent_dependency_loader.py +2 -2
  195. claude_mpm/utils/dependency_strategies.py +8 -3
  196. claude_mpm/utils/environment_context.py +2 -2
  197. claude_mpm/utils/error_handler.py +2 -2
  198. claude_mpm/utils/file_utils.py +1 -1
  199. claude_mpm/utils/imports.py +1 -1
  200. claude_mpm/utils/log_cleanup.py +21 -7
  201. claude_mpm/validation/agent_validator.py +2 -2
  202. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/METADATA +4 -1
  203. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/RECORD +207 -200
  204. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/WHEEL +0 -0
  205. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/entry_points.txt +0 -0
  206. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/licenses/LICENSE +0 -0
  207. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,439 @@
1
+ """
2
+ MCP Configuration Manager
3
+ ========================
4
+
5
+ Manages MCP service configurations, preferring pipx installations
6
+ over local virtual environments for better isolation and management.
7
+
8
+ This module provides utilities to detect, configure, and validate
9
+ MCP service installations.
10
+ """
11
+
12
+ import json
13
+ import subprocess
14
+ from datetime import datetime, timezone
15
+ from enum import Enum
16
+ from pathlib import Path
17
+ from typing import Dict, Optional, Tuple
18
+
19
+ from ..core.logger import get_logger
20
+
21
+
22
+ class ConfigLocation(Enum):
23
+ """Enumeration of Claude configuration file locations."""
24
+
25
+ CLAUDE_JSON = Path.home() / ".claude.json" # Primary Claude config
26
+ CLAUDE_DESKTOP = (
27
+ Path.home() / ".claude" / "claude_desktop_config.json"
28
+ ) # Not used by Claude Code
29
+ PROJECT_MCP = ".mcp.json" # Project-level MCP config (deprecated)
30
+
31
+
32
+ class MCPConfigManager:
33
+ """Manages MCP service configurations with pipx preference."""
34
+
35
+ # Standard MCP services that should use pipx
36
+ PIPX_SERVICES = {
37
+ "mcp-vector-search",
38
+ "mcp-browser",
39
+ "mcp-ticketer",
40
+ }
41
+
42
+ def __init__(self):
43
+ """Initialize the MCP configuration manager."""
44
+ self.logger = get_logger(__name__)
45
+ self.pipx_base = Path.home() / ".local" / "pipx" / "venvs"
46
+ self.project_root = Path.cwd()
47
+
48
+ # Use the proper Claude config file location
49
+ self.claude_config_path = ConfigLocation.CLAUDE_JSON.value
50
+
51
+ def detect_service_path(self, service_name: str) -> Optional[str]:
52
+ """
53
+ Detect the best path for an MCP service.
54
+
55
+ Priority order:
56
+ 1. Pipx installation (preferred)
57
+ 2. System PATH (likely from pipx)
58
+ 3. Local venv (fallback)
59
+
60
+ Args:
61
+ service_name: Name of the MCP service
62
+
63
+ Returns:
64
+ Path to the service executable or None if not found
65
+ """
66
+ # Check pipx installation first
67
+ pipx_path = self._check_pipx_installation(service_name)
68
+ if pipx_path:
69
+ self.logger.debug(f"Found {service_name} via pipx: {pipx_path}")
70
+ return pipx_path
71
+
72
+ # Check system PATH
73
+ system_path = self._check_system_path(service_name)
74
+ if system_path:
75
+ self.logger.debug(f"Found {service_name} in PATH: {system_path}")
76
+ return system_path
77
+
78
+ # Fallback to local venv
79
+ local_path = self._check_local_venv(service_name)
80
+ if local_path:
81
+ self.logger.warning(
82
+ f"Using local venv for {service_name} (consider installing via pipx)"
83
+ )
84
+ return local_path
85
+
86
+ self.logger.warning(f"Service {service_name} not found")
87
+ return None
88
+
89
+ def _check_pipx_installation(self, service_name: str) -> Optional[str]:
90
+ """Check if service is installed via pipx."""
91
+ pipx_venv = self.pipx_base / service_name
92
+
93
+ if not pipx_venv.exists():
94
+ return None
95
+
96
+ # Special handling for mcp-vector-search (needs Python interpreter)
97
+ if service_name == "mcp-vector-search":
98
+ python_bin = pipx_venv / "bin" / "python"
99
+ if python_bin.exists() and python_bin.is_file():
100
+ return str(python_bin)
101
+ else:
102
+ # Other services use direct binary
103
+ service_bin = pipx_venv / "bin" / service_name
104
+ if service_bin.exists() and service_bin.is_file():
105
+ return str(service_bin)
106
+
107
+ return None
108
+
109
+ def _check_system_path(self, service_name: str) -> Optional[str]:
110
+ """Check if service is available in system PATH."""
111
+ try:
112
+ result = subprocess.run(
113
+ ["which", service_name],
114
+ capture_output=True,
115
+ text=True,
116
+ check=False,
117
+ )
118
+ if result.returncode == 0:
119
+ path = result.stdout.strip()
120
+ # Verify it's from pipx
121
+ if "/.local/bin/" in path or "/pipx/" in path:
122
+ return path
123
+ except Exception as e:
124
+ self.logger.debug(f"Error checking system PATH: {e}")
125
+
126
+ return None
127
+
128
+ def _check_local_venv(self, service_name: str) -> Optional[str]:
129
+ """Check for local virtual environment installation (fallback)."""
130
+ # Common local development paths
131
+ possible_paths = [
132
+ Path.home() / "Projects" / "managed" / service_name / ".venv" / "bin",
133
+ self.project_root / ".venv" / "bin",
134
+ self.project_root / "venv" / "bin",
135
+ ]
136
+
137
+ for base_path in possible_paths:
138
+ if service_name == "mcp-vector-search":
139
+ python_bin = base_path / "python"
140
+ if python_bin.exists():
141
+ return str(python_bin)
142
+ else:
143
+ service_bin = base_path / service_name
144
+ if service_bin.exists():
145
+ return str(service_bin)
146
+
147
+ return None
148
+
149
+ def generate_service_config(self, service_name: str) -> Optional[Dict]:
150
+ """
151
+ Generate configuration for a specific MCP service.
152
+
153
+ Args:
154
+ service_name: Name of the MCP service
155
+
156
+ Returns:
157
+ Service configuration dict or None if service not found
158
+ """
159
+ service_path = self.detect_service_path(service_name)
160
+ if not service_path:
161
+ return None
162
+
163
+ config = {
164
+ "type": "stdio",
165
+ "command": service_path,
166
+ }
167
+
168
+ # Service-specific configurations
169
+ if service_name == "mcp-vector-search":
170
+ config["args"] = [
171
+ "-m",
172
+ "mcp_vector_search.mcp.server",
173
+ str(self.project_root),
174
+ ]
175
+ config["env"] = {}
176
+ elif service_name == "mcp-browser":
177
+ config["args"] = ["mcp"]
178
+ config["env"] = {"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")}
179
+ elif service_name == "mcp-ticketer":
180
+ config["args"] = ["mcp"]
181
+ else:
182
+ # Generic config for unknown services
183
+ config["args"] = []
184
+
185
+ return config
186
+
187
+ def ensure_mcp_services_configured(self) -> Tuple[bool, str]:
188
+ """
189
+ Ensure MCP services are configured in ~/.claude.json on startup.
190
+
191
+ This method checks if the core MCP services are configured in the
192
+ current project's mcpServers section and automatically adds them if missing.
193
+
194
+ Returns:
195
+ Tuple of (success, message)
196
+ """
197
+ updated = False
198
+ added_services = []
199
+ project_key = str(self.project_root)
200
+
201
+ # Load existing Claude config or create minimal structure
202
+ claude_config = {}
203
+ if self.claude_config_path.exists():
204
+ try:
205
+ with open(self.claude_config_path) as f:
206
+ claude_config = json.load(f)
207
+ except Exception as e:
208
+ self.logger.error(f"Error reading {self.claude_config_path}: {e}")
209
+ return False, f"Failed to read Claude config: {e}"
210
+
211
+ # Ensure projects structure exists
212
+ if "projects" not in claude_config:
213
+ claude_config["projects"] = {}
214
+
215
+ if project_key not in claude_config["projects"]:
216
+ claude_config["projects"][project_key] = {
217
+ "allowedTools": [],
218
+ "history": [],
219
+ "mcpContextUris": [],
220
+ "mcpServers": {},
221
+ "enabledMcpjsonServers": [],
222
+ "disabledMcpjsonServers": [],
223
+ "hasTrustDialogAccepted": False,
224
+ "projectOnboardingSeenCount": 0,
225
+ "hasClaudeMdExternalIncludesApproved": False,
226
+ "hasClaudeMdExternalIncludesWarningShown": False,
227
+ }
228
+ updated = True
229
+
230
+ # Get the project's mcpServers section
231
+ project_config = claude_config["projects"][project_key]
232
+ if "mcpServers" not in project_config:
233
+ project_config["mcpServers"] = {}
234
+ updated = True
235
+
236
+ # Check each service and add if missing
237
+ for service_name in self.PIPX_SERVICES:
238
+ if service_name not in project_config["mcpServers"]:
239
+ # Try to detect and configure the service
240
+ service_path = self.detect_service_path(service_name)
241
+ if service_path:
242
+ config = self.generate_service_config(service_name)
243
+ if config:
244
+ project_config["mcpServers"][service_name] = config
245
+ added_services.append(service_name)
246
+ updated = True
247
+ self.logger.debug(
248
+ f"Added MCP service to config: {service_name}"
249
+ )
250
+ else:
251
+ self.logger.debug(
252
+ f"MCP service {service_name} not found for auto-configuration"
253
+ )
254
+
255
+ # Write updated config if changes were made
256
+ if updated:
257
+ try:
258
+ # Create backup if file exists and is large (> 100KB)
259
+ if self.claude_config_path.exists():
260
+ file_size = self.claude_config_path.stat().st_size
261
+ if file_size > 100000: # 100KB
262
+ backup_path = self.claude_config_path.with_suffix(
263
+ f".backup.{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.json"
264
+ )
265
+ import shutil
266
+
267
+ shutil.copy2(self.claude_config_path, backup_path)
268
+ self.logger.debug(f"Created backup: {backup_path}")
269
+
270
+ # Write updated config
271
+ with open(self.claude_config_path, "w") as f:
272
+ json.dump(claude_config, f, indent=2)
273
+
274
+ if added_services:
275
+ message = (
276
+ f"Auto-configured MCP services: {', '.join(added_services)}"
277
+ )
278
+ # Don't log here - let the caller handle logging to avoid duplicates
279
+ return True, message
280
+ return True, "All MCP services already configured"
281
+ except Exception as e:
282
+ self.logger.error(f"Failed to write Claude config: {e}")
283
+ return False, f"Failed to write configuration: {e}"
284
+
285
+ return True, "All MCP services already configured"
286
+
287
+ def update_mcp_config(self, force_pipx: bool = True) -> Tuple[bool, str]:
288
+ """
289
+ Update the MCP configuration in ~/.claude.json.
290
+
291
+ Args:
292
+ force_pipx: If True, only use pipx installations
293
+
294
+ Returns:
295
+ Tuple of (success, message)
296
+ """
297
+ # This method now delegates to ensure_mcp_services_configured
298
+ # since we're updating the Claude config directly
299
+ return self.ensure_mcp_services_configured()
300
+
301
+ def update_project_mcp_config(self, force_pipx: bool = True) -> Tuple[bool, str]:
302
+ """
303
+ Update the .mcp.json configuration file (legacy method).
304
+
305
+ Args:
306
+ force_pipx: If True, only use pipx installations
307
+
308
+ Returns:
309
+ Tuple of (success, message)
310
+ """
311
+ mcp_config_path = self.project_root / ConfigLocation.PROJECT_MCP.value
312
+
313
+ # Load existing config if it exists
314
+ existing_config = {}
315
+ if mcp_config_path.exists():
316
+ try:
317
+ with open(mcp_config_path) as f:
318
+ existing_config = json.load(f)
319
+ except Exception as e:
320
+ self.logger.error(f"Error reading existing config: {e}")
321
+
322
+ # Generate new configurations
323
+ new_config = {"mcpServers": {}}
324
+ missing_services = []
325
+
326
+ for service_name in self.PIPX_SERVICES:
327
+ config = self.generate_service_config(service_name)
328
+ if config:
329
+ new_config["mcpServers"][service_name] = config
330
+ elif force_pipx:
331
+ missing_services.append(service_name)
332
+ # Keep existing config if not forcing pipx
333
+ elif service_name in existing_config.get("mcpServers", {}):
334
+ new_config["mcpServers"][service_name] = existing_config["mcpServers"][
335
+ service_name
336
+ ]
337
+
338
+ # Add any additional services from existing config
339
+ for service_name, config in existing_config.get("mcpServers", {}).items():
340
+ if service_name not in new_config["mcpServers"]:
341
+ new_config["mcpServers"][service_name] = config
342
+
343
+ # Write the updated configuration
344
+ try:
345
+ with open(mcp_config_path, "w") as f:
346
+ json.dump(new_config, f, indent=2)
347
+
348
+ if missing_services:
349
+ message = f"Updated .mcp.json. Missing services (install via pipx): {', '.join(missing_services)}"
350
+ return True, message
351
+ return True, "Successfully updated .mcp.json with pipx paths"
352
+ except Exception as e:
353
+ return False, f"Failed to update .mcp.json: {e}"
354
+
355
+ def validate_configuration(self) -> Dict[str, bool]:
356
+ """
357
+ Validate that all configured MCP services are accessible.
358
+
359
+ Returns:
360
+ Dict mapping service names to availability status
361
+ """
362
+ project_key = str(self.project_root)
363
+
364
+ # Check Claude config
365
+ if not self.claude_config_path.exists():
366
+ # Also check legacy .mcp.json
367
+ mcp_config_path = self.project_root / ConfigLocation.PROJECT_MCP.value
368
+ if mcp_config_path.exists():
369
+ try:
370
+ with open(mcp_config_path) as f:
371
+ config = json.load(f)
372
+ results = {}
373
+ for service_name, service_config in config.get(
374
+ "mcpServers", {}
375
+ ).items():
376
+ command_path = service_config.get("command", "")
377
+ results[service_name] = Path(command_path).exists()
378
+ return results
379
+ except Exception:
380
+ pass
381
+ return {}
382
+
383
+ try:
384
+ with open(self.claude_config_path) as f:
385
+ claude_config = json.load(f)
386
+
387
+ # Get project's MCP servers
388
+ if "projects" in claude_config and project_key in claude_config["projects"]:
389
+ mcp_servers = claude_config["projects"][project_key].get(
390
+ "mcpServers", {}
391
+ )
392
+ results = {}
393
+ for service_name, service_config in mcp_servers.items():
394
+ command_path = service_config.get("command", "")
395
+ results[service_name] = Path(command_path).exists()
396
+ return results
397
+ except Exception as e:
398
+ self.logger.error(f"Error reading config: {e}")
399
+
400
+ return {}
401
+
402
+ def install_missing_services(self) -> Tuple[bool, str]:
403
+ """
404
+ Install missing MCP services via pipx.
405
+
406
+ Returns:
407
+ Tuple of (success, message)
408
+ """
409
+ missing = []
410
+ for service_name in self.PIPX_SERVICES:
411
+ if not self.detect_service_path(service_name):
412
+ missing.append(service_name)
413
+
414
+ if not missing:
415
+ return True, "All MCP services are already installed"
416
+
417
+ installed = []
418
+ failed = []
419
+
420
+ for service_name in missing:
421
+ try:
422
+ self.logger.info(f"Installing {service_name} via pipx...")
423
+ subprocess.run(
424
+ ["pipx", "install", service_name],
425
+ capture_output=True,
426
+ text=True,
427
+ check=True,
428
+ )
429
+ installed.append(service_name)
430
+ self.logger.info(f"Successfully installed {service_name}")
431
+ except subprocess.CalledProcessError as e:
432
+ failed.append(service_name)
433
+ self.logger.error(f"Failed to install {service_name}: {e.stderr}")
434
+
435
+ if failed:
436
+ return False, f"Failed to install: {', '.join(failed)}"
437
+ if installed:
438
+ return True, f"Successfully installed: {', '.join(installed)}"
439
+ return True, "No services needed installation"
@@ -26,7 +26,7 @@ __version__ = "0.1.0"
26
26
 
27
27
 
28
28
  # Lazy imports to prevent circular dependencies and improve startup performance
29
- def __getattr__(name):
29
+ def __getattr__(name): # noqa: PLR0911
30
30
  """Lazy import mechanism for MCP Gateway components."""
31
31
 
32
32
  # Core interfaces and base classes
@@ -20,7 +20,7 @@ DESIGN DECISIONS:
20
20
  import json
21
21
  import os
22
22
  import sys
23
- from datetime import datetime
23
+ from datetime import datetime, timezone
24
24
  from pathlib import Path
25
25
  from typing import Any, Dict, Optional
26
26
 
@@ -144,7 +144,7 @@ class MCPAutoConfigurator:
144
144
  prefs = {
145
145
  "asked": True,
146
146
  "choice": choice,
147
- "timestamp": datetime.now().isoformat(),
147
+ "timestamp": datetime.now(timezone.utc).isoformat(),
148
148
  }
149
149
 
150
150
  try:
@@ -266,7 +266,7 @@ class MCPAutoConfigurator:
266
266
  def _create_backup(self) -> Optional[Path]:
267
267
  """Create backup of existing configuration."""
268
268
  try:
269
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
269
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
270
270
  backup_path = self.claude_config_path.with_suffix(
271
271
  f".backup.{timestamp}.json"
272
272
  )
@@ -143,7 +143,7 @@ class MCPConfigLoader:
143
143
  import json
144
144
 
145
145
  current[key] = json.loads(env_value)
146
- except:
146
+ except Exception:
147
147
  # Fall back to string value
148
148
  current[key] = env_value
149
149
 
@@ -60,6 +60,23 @@ class MCPConfiguration(BaseMCPService, IMCPConfiguration):
60
60
  "timeout_default": 30, # seconds
61
61
  "max_concurrent": 10,
62
62
  },
63
+ "external_services": {
64
+ "enabled": True,
65
+ "auto_install": True,
66
+ "services": [
67
+ {
68
+ "name": "mcp-vector-search",
69
+ "package": "mcp-vector-search",
70
+ "enabled": True,
71
+ "auto_index": True,
72
+ },
73
+ {
74
+ "name": "mcp-browser",
75
+ "package": "mcp-browser",
76
+ "enabled": True,
77
+ },
78
+ ],
79
+ },
63
80
  "logging": {
64
81
  "level": "INFO",
65
82
  "file": "~/.claude/logs/mcp_gateway.log",
@@ -208,7 +225,7 @@ class MCPConfiguration(BaseMCPService, IMCPConfiguration):
208
225
  import json
209
226
 
210
227
  current[key] = json.loads(env_value)
211
- except:
228
+ except Exception:
212
229
  # Fall back to string value
213
230
  current[key] = env_value
214
231
 
@@ -279,10 +279,10 @@ class BaseMCPService(BaseService):
279
279
  healthy: Whether service is healthy
280
280
  details: Additional health details
281
281
  """
282
- from datetime import datetime
282
+ from datetime import datetime, timezone
283
283
 
284
284
  self._health_status["healthy"] = healthy
285
- self._health_status["last_check"] = datetime.now().isoformat()
285
+ self._health_status["last_check"] = datetime.now(timezone.utc).isoformat()
286
286
 
287
287
  if details:
288
288
  self._health_status["details"].update(details)
@@ -115,6 +115,14 @@ except ImportError:
115
115
  # Unified ticket tool is optional
116
116
  UnifiedTicketTool = None
117
117
 
118
+ try:
119
+ from claude_mpm.services.mcp_gateway.tools.external_mcp_services import (
120
+ ExternalMCPServiceManager,
121
+ )
122
+ except ImportError:
123
+ # External MCP services are optional
124
+ ExternalMCPServiceManager = None
125
+
118
126
  # Manager module removed - using simplified architecture
119
127
 
120
128
 
@@ -148,6 +156,7 @@ class MCPGatewayOrchestrator:
148
156
  self.registry: Optional[ToolRegistry] = None
149
157
  self.communication: Optional[StdioHandler] = None
150
158
  self.configuration: Optional[MCPConfiguration] = None
159
+ self.external_services: Optional[ExternalMCPServiceManager] = None
151
160
 
152
161
  # Shutdown handling
153
162
  self._shutdown_event = asyncio.Event()
@@ -199,6 +208,42 @@ class MCPGatewayOrchestrator:
199
208
  self.logger.warning(f"Failed to register some tools: {e}")
200
209
  # Continue - server can run with partial tools
201
210
 
211
+ # Initialize external MCP services if available
212
+ if ExternalMCPServiceManager is not None:
213
+ try:
214
+ self.logger.info("Initializing external MCP services...")
215
+ self.external_services = ExternalMCPServiceManager()
216
+ external_services = (
217
+ await self.external_services.initialize_services()
218
+ )
219
+
220
+ if external_services and self.registry:
221
+ for service in external_services:
222
+ try:
223
+ if self.registry.register_tool(
224
+ service, category="external"
225
+ ):
226
+ self.logger.info(
227
+ f"Registered external service: {service.service_name}"
228
+ )
229
+ else:
230
+ self.logger.warning(
231
+ f"Failed to register external service: {service.service_name}"
232
+ )
233
+ except Exception as e:
234
+ self.logger.warning(
235
+ f"Error registering {service.service_name}: {e}"
236
+ )
237
+
238
+ self.logger.info(
239
+ f"Initialized {len(external_services)} external MCP services"
240
+ )
241
+ except Exception as e:
242
+ self.logger.warning(
243
+ f"Failed to initialize external MCP services: {e}"
244
+ )
245
+ self.external_services = None
246
+
202
247
  # Initialize communication handler with fallback
203
248
  try:
204
249
  self.communication = StdioHandler()
@@ -371,6 +416,13 @@ class MCPGatewayOrchestrator:
371
416
  except Exception as e:
372
417
  self.logger.warning(f"Error during communication shutdown: {e}")
373
418
 
419
+ # Shutdown external services
420
+ if self.external_services:
421
+ try:
422
+ await self.external_services.shutdown()
423
+ except Exception as e:
424
+ self.logger.warning(f"Error during external services shutdown: {e}")
425
+
374
426
  self.logger.info("MCP Gateway shutdown complete")
375
427
 
376
428
  except Exception as e:
@@ -11,7 +11,7 @@ Part of ISS-0035: MCP Server Implementation - Core Server and Tool Registry
11
11
  import asyncio
12
12
  import re
13
13
  import traceback
14
- from datetime import datetime
14
+ from datetime import datetime, timezone
15
15
  from threading import RLock
16
16
  from typing import Any, Dict, List, Optional, Set
17
17
 
@@ -159,9 +159,9 @@ class ToolRegistry(BaseMCPService, IMCPToolRegistry):
159
159
 
160
160
  # Update metrics
161
161
  self._metrics["total_tools"] = len(self._adapters)
162
- self._metrics["registration_time"][
163
- tool_name
164
- ] = datetime.now().isoformat()
162
+ self._metrics["registration_time"][tool_name] = datetime.now(
163
+ timezone.utc
164
+ ).isoformat()
165
165
  self._metrics["invocations"][tool_name] = 0
166
166
  self._metrics["errors"][tool_name] = 0
167
167
 
@@ -272,7 +272,7 @@ class ToolRegistry(BaseMCPService, IMCPToolRegistry):
272
272
  error handling, metrics tracking, and validation.
273
273
  """
274
274
  tool_name = invocation.tool_name
275
- start_time = datetime.now()
275
+ start_time = datetime.now(timezone.utc)
276
276
 
277
277
  try:
278
278
  self.log_info(f"Invoking tool: {tool_name}")
@@ -300,7 +300,7 @@ class ToolRegistry(BaseMCPService, IMCPToolRegistry):
300
300
  result = await adapter.invoke(invocation)
301
301
 
302
302
  # Calculate execution time
303
- execution_time = (datetime.now() - start_time).total_seconds()
303
+ execution_time = (datetime.now(timezone.utc) - start_time).total_seconds()
304
304
  result.execution_time = execution_time
305
305
 
306
306
  # Update metrics
@@ -308,7 +308,9 @@ class ToolRegistry(BaseMCPService, IMCPToolRegistry):
308
308
  self._metrics["invocations"][tool_name] = (
309
309
  self._metrics["invocations"].get(tool_name, 0) + 1
310
310
  )
311
- self._metrics["last_invocation"][tool_name] = datetime.now().isoformat()
311
+ self._metrics["last_invocation"][tool_name] = datetime.now(
312
+ timezone.utc
313
+ ).isoformat()
312
314
 
313
315
  if not result.success:
314
316
  self._metrics["errors"][tool_name] = (
@@ -331,7 +333,7 @@ class ToolRegistry(BaseMCPService, IMCPToolRegistry):
331
333
  self._metrics["errors"].get(tool_name, 0) + 1
332
334
  )
333
335
 
334
- execution_time = (datetime.now() - start_time).total_seconds()
336
+ execution_time = (datetime.now(timezone.utc) - start_time).total_seconds()
335
337
 
336
338
  return MCPToolResult(
337
339
  success=False, error=error_msg, execution_time=execution_time