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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +390 -28
- claude_mpm/agents/templates/data_engineer.json +39 -14
- claude_mpm/agents/templates/research.json +20 -8
- claude_mpm/agents/templates/web_qa.json +25 -10
- claude_mpm/cli/__init__.py +1 -0
- claude_mpm/cli/commands/agent_manager.py +3 -3
- claude_mpm/cli/commands/agents.py +2 -2
- claude_mpm/cli/commands/aggregate.py +1 -1
- claude_mpm/cli/commands/config.py +2 -2
- claude_mpm/cli/commands/configure.py +5 -5
- claude_mpm/cli/commands/configure_tui.py +7 -7
- claude_mpm/cli/commands/dashboard.py +1 -1
- claude_mpm/cli/commands/debug.py +5 -5
- claude_mpm/cli/commands/mcp.py +1 -1
- claude_mpm/cli/commands/mcp_command_router.py +12 -1
- claude_mpm/cli/commands/mcp_config.py +154 -0
- claude_mpm/cli/commands/mcp_external_commands.py +249 -0
- claude_mpm/cli/commands/mcp_install_commands.py +93 -24
- claude_mpm/cli/commands/mcp_setup_external.py +870 -0
- claude_mpm/cli/commands/monitor.py +2 -2
- claude_mpm/cli/commands/mpm_init_handler.py +1 -1
- claude_mpm/cli/commands/run.py +114 -0
- claude_mpm/cli/commands/search.py +292 -0
- claude_mpm/cli/interactive/agent_wizard.py +2 -2
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/mcp_parser.py +15 -0
- claude_mpm/cli/parsers/run_parser.py +5 -0
- claude_mpm/cli/parsers/search_parser.py +245 -0
- claude_mpm/cli/startup_logging.py +3 -5
- claude_mpm/cli/utils.py +1 -1
- claude_mpm/constants.py +1 -0
- claude_mpm/core/agent_registry.py +12 -8
- claude_mpm/core/agent_session_manager.py +8 -8
- claude_mpm/core/api_validator.py +4 -4
- claude_mpm/core/base_service.py +10 -10
- claude_mpm/core/cache.py +5 -5
- claude_mpm/core/config_constants.py +1 -1
- claude_mpm/core/container.py +1 -1
- claude_mpm/core/error_handler.py +2 -2
- claude_mpm/core/file_utils.py +1 -1
- claude_mpm/core/framework_loader.py +3 -3
- claude_mpm/core/hook_manager.py +8 -6
- claude_mpm/core/instruction_reinforcement_hook.py +2 -2
- claude_mpm/core/interactive_session.py +1 -1
- claude_mpm/core/lazy.py +3 -3
- claude_mpm/core/log_manager.py +16 -12
- claude_mpm/core/logger.py +16 -11
- claude_mpm/core/logging_config.py +4 -2
- claude_mpm/core/oneshot_session.py +1 -1
- claude_mpm/core/optimized_agent_loader.py +6 -6
- claude_mpm/core/output_style_manager.py +1 -1
- claude_mpm/core/pm_hook_interceptor.py +3 -3
- claude_mpm/core/service_registry.py +1 -1
- claude_mpm/core/session_manager.py +11 -9
- claude_mpm/core/socketio_pool.py +13 -13
- claude_mpm/core/types.py +2 -2
- claude_mpm/core/unified_agent_registry.py +9 -2
- claude_mpm/core/unified_paths.py +1 -1
- claude_mpm/dashboard/analysis_runner.py +4 -4
- claude_mpm/dashboard/api/simple_directory.py +1 -1
- claude_mpm/generators/agent_profile_generator.py +4 -2
- claude_mpm/hooks/base_hook.py +2 -2
- claude_mpm/hooks/claude_hooks/connection_pool.py +4 -4
- claude_mpm/hooks/claude_hooks/event_handlers.py +12 -12
- claude_mpm/hooks/claude_hooks/hook_handler.py +4 -4
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +3 -3
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +15 -14
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +4 -4
- claude_mpm/hooks/claude_hooks/installer.py +3 -3
- claude_mpm/hooks/claude_hooks/memory_integration.py +3 -3
- claude_mpm/hooks/claude_hooks/response_tracking.py +3 -3
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +5 -5
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +3 -3
- claude_mpm/hooks/claude_hooks/services/state_manager.py +8 -7
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +3 -3
- claude_mpm/hooks/claude_hooks/tool_analysis.py +2 -2
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/hooks/tool_call_interceptor.py +2 -2
- claude_mpm/models/agent_session.py +5 -5
- claude_mpm/services/__init__.py +1 -1
- claude_mpm/services/agent_capabilities_service.py +1 -1
- claude_mpm/services/agents/agent_builder.py +3 -3
- claude_mpm/services/agents/deployment/agent_deployment.py +29 -13
- claude_mpm/services/agents/deployment/agent_discovery_service.py +22 -6
- claude_mpm/services/agents/deployment/agent_filesystem_manager.py +7 -5
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +3 -1
- claude_mpm/services/agents/deployment/agent_metrics_collector.py +1 -1
- claude_mpm/services/agents/deployment/agent_operation_service.py +2 -2
- claude_mpm/services/agents/deployment/agent_state_service.py +2 -2
- claude_mpm/services/agents/deployment/agent_template_builder.py +1 -1
- claude_mpm/services/agents/deployment/agent_versioning.py +1 -1
- claude_mpm/services/agents/deployment/deployment_wrapper.py +2 -3
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +6 -4
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +1 -1
- claude_mpm/services/agents/loading/agent_profile_loader.py +5 -3
- claude_mpm/services/agents/loading/base_agent_manager.py +2 -2
- claude_mpm/services/agents/local_template_manager.py +6 -6
- claude_mpm/services/agents/management/agent_management_service.py +3 -3
- claude_mpm/services/agents/memory/content_manager.py +3 -3
- claude_mpm/services/agents/memory/memory_format_service.py +2 -2
- claude_mpm/services/agents/memory/template_generator.py +3 -3
- claude_mpm/services/agents/registry/__init__.py +1 -1
- claude_mpm/services/agents/registry/modification_tracker.py +2 -2
- claude_mpm/services/async_session_logger.py +3 -3
- claude_mpm/services/claude_session_logger.py +4 -4
- claude_mpm/services/cli/agent_cleanup_service.py +5 -0
- claude_mpm/services/cli/agent_listing_service.py +1 -1
- claude_mpm/services/cli/agent_validation_service.py +1 -0
- claude_mpm/services/cli/memory_crud_service.py +11 -6
- claude_mpm/services/cli/memory_output_formatter.py +1 -1
- claude_mpm/services/cli/session_manager.py +15 -11
- claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
- claude_mpm/services/core/memory_manager.py +81 -23
- claude_mpm/services/core/path_resolver.py +2 -2
- claude_mpm/services/diagnostics/checks/installation_check.py +1 -1
- claude_mpm/services/event_aggregator.py +4 -2
- claude_mpm/services/event_bus/direct_relay.py +5 -3
- claude_mpm/services/event_bus/event_bus.py +3 -3
- claude_mpm/services/event_bus/relay.py +6 -4
- claude_mpm/services/events/consumers/dead_letter.py +5 -3
- claude_mpm/services/events/core.py +3 -3
- claude_mpm/services/events/producers/hook.py +6 -6
- claude_mpm/services/events/producers/system.py +8 -8
- claude_mpm/services/exceptions.py +5 -5
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +3 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +2 -2
- claude_mpm/services/hook_installer_service.py +1 -1
- claude_mpm/services/infrastructure/context_preservation.py +6 -4
- claude_mpm/services/infrastructure/daemon_manager.py +2 -2
- claude_mpm/services/infrastructure/logging.py +2 -2
- claude_mpm/services/mcp_config_manager.py +439 -0
- claude_mpm/services/mcp_gateway/__init__.py +1 -1
- claude_mpm/services/mcp_gateway/auto_configure.py +3 -3
- claude_mpm/services/mcp_gateway/config/config_loader.py +1 -1
- claude_mpm/services/mcp_gateway/config/configuration.py +18 -1
- claude_mpm/services/mcp_gateway/core/base.py +2 -2
- claude_mpm/services/mcp_gateway/main.py +52 -0
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +10 -8
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +4 -4
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +1 -1
- claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -3
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +15 -15
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +7 -5
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +443 -0
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +5 -5
- claude_mpm/services/mcp_gateway/tools/hello_world.py +9 -9
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +16 -16
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +17 -17
- claude_mpm/services/memory/builder.py +7 -5
- claude_mpm/services/memory/indexed_memory.py +4 -4
- claude_mpm/services/memory/optimizer.py +6 -6
- claude_mpm/services/memory/router.py +3 -3
- claude_mpm/services/monitor/daemon.py +1 -1
- claude_mpm/services/monitor/daemon_manager.py +6 -6
- claude_mpm/services/monitor/event_emitter.py +2 -2
- claude_mpm/services/monitor/handlers/file.py +1 -1
- claude_mpm/services/monitor/management/lifecycle.py +1 -1
- claude_mpm/services/monitor/server.py +4 -4
- claude_mpm/services/monitor_build_service.py +2 -2
- claude_mpm/services/port_manager.py +2 -2
- claude_mpm/services/response_tracker.py +2 -2
- claude_mpm/services/session_management_service.py +3 -2
- claude_mpm/services/socketio/client_proxy.py +2 -2
- claude_mpm/services/socketio/dashboard_server.py +4 -3
- claude_mpm/services/socketio/event_normalizer.py +12 -8
- claude_mpm/services/socketio/handlers/base.py +2 -2
- claude_mpm/services/socketio/handlers/connection.py +10 -10
- claude_mpm/services/socketio/handlers/connection_handler.py +13 -10
- claude_mpm/services/socketio/handlers/file.py +1 -1
- claude_mpm/services/socketio/handlers/git.py +1 -1
- claude_mpm/services/socketio/handlers/hook.py +16 -15
- claude_mpm/services/socketio/migration_utils.py +1 -1
- claude_mpm/services/socketio/monitor_client.py +5 -5
- claude_mpm/services/socketio/server/broadcaster.py +9 -7
- claude_mpm/services/socketio/server/connection_manager.py +2 -2
- claude_mpm/services/socketio/server/core.py +7 -5
- claude_mpm/services/socketio/server/eventbus_integration.py +18 -11
- claude_mpm/services/socketio/server/main.py +13 -13
- claude_mpm/services/socketio_client_manager.py +4 -4
- claude_mpm/services/system_instructions_service.py +2 -2
- claude_mpm/services/ticket_services/validation_service.py +1 -1
- claude_mpm/services/utility_service.py +5 -2
- claude_mpm/services/version_control/branch_strategy.py +2 -2
- claude_mpm/services/version_control/git_operations.py +22 -20
- claude_mpm/services/version_control/semantic_versioning.py +3 -3
- claude_mpm/services/version_control/version_parser.py +7 -5
- claude_mpm/services/visualization/mermaid_generator.py +1 -1
- claude_mpm/storage/state_storage.py +1 -1
- claude_mpm/tools/code_tree_analyzer.py +19 -18
- claude_mpm/tools/code_tree_builder.py +2 -2
- claude_mpm/tools/code_tree_events.py +10 -8
- claude_mpm/tools/socketio_debug.py +3 -3
- claude_mpm/utils/agent_dependency_loader.py +2 -2
- claude_mpm/utils/dependency_strategies.py +8 -3
- claude_mpm/utils/environment_context.py +2 -2
- claude_mpm/utils/error_handler.py +2 -2
- claude_mpm/utils/file_utils.py +1 -1
- claude_mpm/utils/imports.py +1 -1
- claude_mpm/utils/log_cleanup.py +21 -7
- claude_mpm/validation/agent_validator.py +2 -2
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/METADATA +4 -1
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/RECORD +207 -200
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.13.dist-info}/licenses/LICENSE +0 -0
- {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
|
)
|
|
@@ -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
|
-
|
|
164
|
-
|
|
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(
|
|
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
|