claude-mpm 4.3.11__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.
- claude_mpm/VERSION +1 -1
- 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/mcp_command_router.py +11 -0
- claude_mpm/cli/commands/mcp_config.py +157 -0
- claude_mpm/cli/commands/mcp_external_commands.py +241 -0
- claude_mpm/cli/commands/mcp_install_commands.py +64 -23
- claude_mpm/cli/commands/mcp_setup_external.py +829 -0
- claude_mpm/cli/commands/run.py +70 -0
- claude_mpm/cli/commands/search.py +285 -0
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/mcp_parser.py +17 -0
- claude_mpm/cli/parsers/run_parser.py +5 -0
- claude_mpm/cli/parsers/search_parser.py +239 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/unified_agent_registry.py +7 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +28 -13
- claude_mpm/services/agents/deployment/agent_discovery_service.py +16 -6
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +6 -4
- claude_mpm/services/cli/agent_cleanup_service.py +5 -0
- claude_mpm/services/mcp_config_manager.py +294 -0
- claude_mpm/services/mcp_gateway/config/configuration.py +17 -0
- claude_mpm/services/mcp_gateway/main.py +38 -0
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +390 -0
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/METADATA +4 -1
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/RECORD +31 -24
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/top_level.txt +0 -0
|
@@ -460,6 +460,13 @@ class UnifiedAgentRegistry:
|
|
|
460
460
|
if agent_format == AgentFormat.JSON:
|
|
461
461
|
data = json.loads(content)
|
|
462
462
|
|
|
463
|
+
# Ensure data is a dictionary, not a list
|
|
464
|
+
if not isinstance(data, dict):
|
|
465
|
+
logger.warning(
|
|
466
|
+
f"Invalid JSON structure in {file_path}: expected object, got {type(data).__name__}"
|
|
467
|
+
)
|
|
468
|
+
return "", []
|
|
469
|
+
|
|
463
470
|
# Handle local agent JSON templates with metadata structure
|
|
464
471
|
if "metadata" in data:
|
|
465
472
|
metadata = data["metadata"]
|
|
@@ -776,16 +776,6 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
776
776
|
agent_sources=agent_sources,
|
|
777
777
|
)
|
|
778
778
|
|
|
779
|
-
# Log version upgrades and source changes
|
|
780
|
-
if comparison_results.get("version_upgrades"):
|
|
781
|
-
self.logger.info(
|
|
782
|
-
f"Version upgrades available for {len(comparison_results['version_upgrades'])} agents"
|
|
783
|
-
)
|
|
784
|
-
if comparison_results.get("source_changes"):
|
|
785
|
-
self.logger.info(
|
|
786
|
-
f"Source changes for {len(comparison_results['source_changes'])} agents"
|
|
787
|
-
)
|
|
788
|
-
|
|
789
779
|
# Filter agents based on comparison results (unless force_rebuild is set)
|
|
790
780
|
if not force_rebuild:
|
|
791
781
|
# Only deploy agents that need updates or are new
|
|
@@ -804,11 +794,36 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
804
794
|
for name, path in agents_to_deploy.items()
|
|
805
795
|
if name in agents_needing_update
|
|
806
796
|
}
|
|
797
|
+
|
|
798
|
+
# Only log upgrade messages if we're actually going to deploy them
|
|
799
|
+
if filtered_agents and comparison_results.get("version_upgrades"):
|
|
800
|
+
# Filter upgrades to only those actually being deployed
|
|
801
|
+
deployed_upgrades = [
|
|
802
|
+
upgrade for upgrade in comparison_results["version_upgrades"]
|
|
803
|
+
if upgrade["name"] in filtered_agents
|
|
804
|
+
]
|
|
805
|
+
|
|
806
|
+
if deployed_upgrades:
|
|
807
|
+
self.logger.info(
|
|
808
|
+
f"Deploying {len(deployed_upgrades)} agent upgrade(s):"
|
|
809
|
+
)
|
|
810
|
+
for upgrade in deployed_upgrades:
|
|
811
|
+
self.logger.info(
|
|
812
|
+
f" Upgrading: {upgrade['name']} "
|
|
813
|
+
f"{upgrade['deployed_version']} -> {upgrade['new_version']} "
|
|
814
|
+
f"(from {upgrade['source']})"
|
|
815
|
+
)
|
|
816
|
+
|
|
807
817
|
agents_to_deploy = filtered_agents
|
|
808
818
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
819
|
+
if agents_to_deploy:
|
|
820
|
+
self.logger.info(
|
|
821
|
+
f"Deploying {len(agents_to_deploy)} agents that need updates"
|
|
822
|
+
)
|
|
823
|
+
else:
|
|
824
|
+
self.logger.debug(
|
|
825
|
+
f"All {len(comparison_results.get('up_to_date', []))} agents are up to date"
|
|
826
|
+
)
|
|
812
827
|
|
|
813
828
|
# Convert to list of Path objects
|
|
814
829
|
template_files = list(agents_to_deploy.values())
|
|
@@ -216,25 +216,35 @@ class AgentDiscoveryService:
|
|
|
216
216
|
metadata = template_data.get("metadata", {})
|
|
217
217
|
capabilities = template_data.get("capabilities", {})
|
|
218
218
|
|
|
219
|
+
# Handle capabilities as either dict or list
|
|
220
|
+
if isinstance(capabilities, list):
|
|
221
|
+
# If capabilities is a list (like in php-engineer.json), treat it as capabilities list
|
|
222
|
+
tools_list = template_data.get("tools", []) # Look for tools at root level
|
|
223
|
+
model_value = template_data.get("model", "sonnet")
|
|
224
|
+
else:
|
|
225
|
+
# If capabilities is a dict, extract tools and model from it
|
|
226
|
+
tools_list = capabilities.get("tools", [])
|
|
227
|
+
model_value = capabilities.get("model", "sonnet")
|
|
228
|
+
|
|
219
229
|
agent_info = {
|
|
220
230
|
"name": metadata.get("name", template_file.stem),
|
|
221
|
-
"description": metadata.get("description", "No description available"),
|
|
231
|
+
"description": metadata.get("description", template_data.get("description", "No description available")),
|
|
222
232
|
"type": template_data.get(
|
|
223
|
-
"agent_type", metadata.get("category", "agent")
|
|
233
|
+
"agent_type", metadata.get("category", template_data.get("category", "agent"))
|
|
224
234
|
), # Extract agent type
|
|
225
235
|
"version": template_data.get(
|
|
226
236
|
"agent_version",
|
|
227
237
|
template_data.get("version", metadata.get("version", "1.0.0")),
|
|
228
238
|
),
|
|
229
|
-
"tools":
|
|
239
|
+
"tools": tools_list,
|
|
230
240
|
"specializations": metadata.get(
|
|
231
|
-
"tags", []
|
|
232
|
-
), # Use tags as specializations
|
|
241
|
+
"tags", template_data.get("tags", [])
|
|
242
|
+
), # Use tags as specializations, fallback to root-level tags
|
|
233
243
|
"file": template_file.name,
|
|
234
244
|
"path": str(template_file),
|
|
235
245
|
"file_path": str(template_file), # Keep for backward compatibility
|
|
236
246
|
"size": template_file.stat().st_size,
|
|
237
|
-
"model":
|
|
247
|
+
"model": model_value,
|
|
238
248
|
"author": metadata.get("author", "unknown"),
|
|
239
249
|
}
|
|
240
250
|
|
|
@@ -742,18 +742,20 @@ class MultiSourceAgentDeploymentService:
|
|
|
742
742
|
|
|
743
743
|
self.logger.info(f"Version comparison complete: {', '.join(summary_parts)}")
|
|
744
744
|
|
|
745
|
+
# Don't log upgrades here - let the caller decide when to log
|
|
746
|
+
# This prevents repeated upgrade messages on every startup
|
|
745
747
|
if comparison_results["version_upgrades"]:
|
|
746
748
|
for upgrade in comparison_results["version_upgrades"]:
|
|
747
|
-
self.logger.
|
|
748
|
-
f" Upgrade: {upgrade['name']} "
|
|
749
|
+
self.logger.debug(
|
|
750
|
+
f" Upgrade available: {upgrade['name']} "
|
|
749
751
|
f"{upgrade['deployed_version']} -> {upgrade['new_version']} "
|
|
750
752
|
f"(from {upgrade['source']})"
|
|
751
753
|
)
|
|
752
754
|
|
|
753
755
|
if comparison_results["source_changes"]:
|
|
754
756
|
for change in comparison_results["source_changes"]:
|
|
755
|
-
self.logger.
|
|
756
|
-
f" Source change: {change['name']} "
|
|
757
|
+
self.logger.debug(
|
|
758
|
+
f" Source change available: {change['name']} "
|
|
757
759
|
f"from {change['from_source']} to {change['to_source']}"
|
|
758
760
|
)
|
|
759
761
|
|
|
@@ -138,6 +138,11 @@ class AgentCleanupService(IAgentCleanupService):
|
|
|
138
138
|
if not isinstance(result, dict):
|
|
139
139
|
result = {"success": bool(result)}
|
|
140
140
|
|
|
141
|
+
# Add success flag based on whether there were errors
|
|
142
|
+
if "success" not in result:
|
|
143
|
+
# Consider it successful if no errors occurred
|
|
144
|
+
result["success"] = not bool(result.get("errors"))
|
|
145
|
+
|
|
141
146
|
# Add cleaned_count for backward compatibility
|
|
142
147
|
if "cleaned_count" not in result:
|
|
143
148
|
removed_count = len(result.get("removed", []))
|
|
@@ -0,0 +1,294 @@
|
|
|
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 os
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
from ..core.logger import get_logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MCPConfigManager:
|
|
22
|
+
"""Manages MCP service configurations with pipx preference."""
|
|
23
|
+
|
|
24
|
+
# Standard MCP services that should use pipx
|
|
25
|
+
PIPX_SERVICES = {
|
|
26
|
+
"mcp-vector-search",
|
|
27
|
+
"mcp-browser",
|
|
28
|
+
"mcp-ticketer",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
"""Initialize the MCP configuration manager."""
|
|
33
|
+
self.logger = get_logger(__name__)
|
|
34
|
+
self.pipx_base = Path.home() / ".local" / "pipx" / "venvs"
|
|
35
|
+
self.project_root = Path.cwd()
|
|
36
|
+
|
|
37
|
+
def detect_service_path(self, service_name: str) -> Optional[str]:
|
|
38
|
+
"""
|
|
39
|
+
Detect the best path for an MCP service.
|
|
40
|
+
|
|
41
|
+
Priority order:
|
|
42
|
+
1. Pipx installation (preferred)
|
|
43
|
+
2. System PATH (likely from pipx)
|
|
44
|
+
3. Local venv (fallback)
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
service_name: Name of the MCP service
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Path to the service executable or None if not found
|
|
51
|
+
"""
|
|
52
|
+
# Check pipx installation first
|
|
53
|
+
pipx_path = self._check_pipx_installation(service_name)
|
|
54
|
+
if pipx_path:
|
|
55
|
+
self.logger.debug(f"Found {service_name} via pipx: {pipx_path}")
|
|
56
|
+
return pipx_path
|
|
57
|
+
|
|
58
|
+
# Check system PATH
|
|
59
|
+
system_path = self._check_system_path(service_name)
|
|
60
|
+
if system_path:
|
|
61
|
+
self.logger.debug(f"Found {service_name} in PATH: {system_path}")
|
|
62
|
+
return system_path
|
|
63
|
+
|
|
64
|
+
# Fallback to local venv
|
|
65
|
+
local_path = self._check_local_venv(service_name)
|
|
66
|
+
if local_path:
|
|
67
|
+
self.logger.warning(
|
|
68
|
+
f"Using local venv for {service_name} (consider installing via pipx)"
|
|
69
|
+
)
|
|
70
|
+
return local_path
|
|
71
|
+
|
|
72
|
+
self.logger.warning(f"Service {service_name} not found")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def _check_pipx_installation(self, service_name: str) -> Optional[str]:
|
|
76
|
+
"""Check if service is installed via pipx."""
|
|
77
|
+
pipx_venv = self.pipx_base / service_name
|
|
78
|
+
|
|
79
|
+
if not pipx_venv.exists():
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
# Special handling for mcp-vector-search (needs Python interpreter)
|
|
83
|
+
if service_name == "mcp-vector-search":
|
|
84
|
+
python_bin = pipx_venv / "bin" / "python"
|
|
85
|
+
if python_bin.exists() and python_bin.is_file():
|
|
86
|
+
return str(python_bin)
|
|
87
|
+
else:
|
|
88
|
+
# Other services use direct binary
|
|
89
|
+
service_bin = pipx_venv / "bin" / service_name
|
|
90
|
+
if service_bin.exists() and service_bin.is_file():
|
|
91
|
+
return str(service_bin)
|
|
92
|
+
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def _check_system_path(self, service_name: str) -> Optional[str]:
|
|
96
|
+
"""Check if service is available in system PATH."""
|
|
97
|
+
try:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["which", service_name],
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
check=False,
|
|
103
|
+
)
|
|
104
|
+
if result.returncode == 0:
|
|
105
|
+
path = result.stdout.strip()
|
|
106
|
+
# Verify it's from pipx
|
|
107
|
+
if "/.local/bin/" in path or "/pipx/" in path:
|
|
108
|
+
return path
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self.logger.debug(f"Error checking system PATH: {e}")
|
|
111
|
+
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
def _check_local_venv(self, service_name: str) -> Optional[str]:
|
|
115
|
+
"""Check for local virtual environment installation (fallback)."""
|
|
116
|
+
# Common local development paths
|
|
117
|
+
possible_paths = [
|
|
118
|
+
Path.home() / "Projects" / "managed" / service_name / ".venv" / "bin",
|
|
119
|
+
self.project_root / ".venv" / "bin",
|
|
120
|
+
self.project_root / "venv" / "bin",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
for base_path in possible_paths:
|
|
124
|
+
if service_name == "mcp-vector-search":
|
|
125
|
+
python_bin = base_path / "python"
|
|
126
|
+
if python_bin.exists():
|
|
127
|
+
return str(python_bin)
|
|
128
|
+
else:
|
|
129
|
+
service_bin = base_path / service_name
|
|
130
|
+
if service_bin.exists():
|
|
131
|
+
return str(service_bin)
|
|
132
|
+
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def generate_service_config(self, service_name: str) -> Optional[Dict]:
|
|
136
|
+
"""
|
|
137
|
+
Generate configuration for a specific MCP service.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
service_name: Name of the MCP service
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Service configuration dict or None if service not found
|
|
144
|
+
"""
|
|
145
|
+
service_path = self.detect_service_path(service_name)
|
|
146
|
+
if not service_path:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
config = {
|
|
150
|
+
"type": "stdio",
|
|
151
|
+
"command": service_path,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Service-specific configurations
|
|
155
|
+
if service_name == "mcp-vector-search":
|
|
156
|
+
config["args"] = [
|
|
157
|
+
"-m",
|
|
158
|
+
"mcp_vector_search.mcp.server",
|
|
159
|
+
str(self.project_root),
|
|
160
|
+
]
|
|
161
|
+
config["env"] = {}
|
|
162
|
+
elif service_name == "mcp-browser":
|
|
163
|
+
config["args"] = ["mcp"]
|
|
164
|
+
config["env"] = {
|
|
165
|
+
"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")
|
|
166
|
+
}
|
|
167
|
+
elif service_name == "mcp-ticketer":
|
|
168
|
+
config["args"] = ["mcp"]
|
|
169
|
+
else:
|
|
170
|
+
# Generic config for unknown services
|
|
171
|
+
config["args"] = []
|
|
172
|
+
|
|
173
|
+
return config
|
|
174
|
+
|
|
175
|
+
def update_mcp_config(self, force_pipx: bool = True) -> Tuple[bool, str]:
|
|
176
|
+
"""
|
|
177
|
+
Update the .mcp.json configuration file.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
force_pipx: If True, only use pipx installations
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Tuple of (success, message)
|
|
184
|
+
"""
|
|
185
|
+
mcp_config_path = self.project_root / ".mcp.json"
|
|
186
|
+
|
|
187
|
+
# Load existing config if it exists
|
|
188
|
+
existing_config = {}
|
|
189
|
+
if mcp_config_path.exists():
|
|
190
|
+
try:
|
|
191
|
+
with open(mcp_config_path, "r") as f:
|
|
192
|
+
existing_config = json.load(f)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
self.logger.error(f"Error reading existing config: {e}")
|
|
195
|
+
|
|
196
|
+
# Generate new configurations
|
|
197
|
+
new_config = {"mcpServers": {}}
|
|
198
|
+
missing_services = []
|
|
199
|
+
|
|
200
|
+
for service_name in self.PIPX_SERVICES:
|
|
201
|
+
config = self.generate_service_config(service_name)
|
|
202
|
+
if config:
|
|
203
|
+
new_config["mcpServers"][service_name] = config
|
|
204
|
+
elif force_pipx:
|
|
205
|
+
missing_services.append(service_name)
|
|
206
|
+
else:
|
|
207
|
+
# Keep existing config if not forcing pipx
|
|
208
|
+
if service_name in existing_config.get("mcpServers", {}):
|
|
209
|
+
new_config["mcpServers"][service_name] = existing_config[
|
|
210
|
+
"mcpServers"
|
|
211
|
+
][service_name]
|
|
212
|
+
|
|
213
|
+
# Add any additional services from existing config
|
|
214
|
+
for service_name, config in existing_config.get("mcpServers", {}).items():
|
|
215
|
+
if service_name not in new_config["mcpServers"]:
|
|
216
|
+
new_config["mcpServers"][service_name] = config
|
|
217
|
+
|
|
218
|
+
# Write the updated configuration
|
|
219
|
+
try:
|
|
220
|
+
with open(mcp_config_path, "w") as f:
|
|
221
|
+
json.dump(new_config, f, indent=2)
|
|
222
|
+
|
|
223
|
+
if missing_services:
|
|
224
|
+
message = f"Updated .mcp.json. Missing services (install via pipx): {', '.join(missing_services)}"
|
|
225
|
+
return True, message
|
|
226
|
+
else:
|
|
227
|
+
return True, "Successfully updated .mcp.json with pipx paths"
|
|
228
|
+
except Exception as e:
|
|
229
|
+
return False, f"Failed to update .mcp.json: {e}"
|
|
230
|
+
|
|
231
|
+
def validate_configuration(self) -> Dict[str, bool]:
|
|
232
|
+
"""
|
|
233
|
+
Validate that all configured MCP services are accessible.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Dict mapping service names to availability status
|
|
237
|
+
"""
|
|
238
|
+
mcp_config_path = self.project_root / ".mcp.json"
|
|
239
|
+
if not mcp_config_path.exists():
|
|
240
|
+
return {}
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
with open(mcp_config_path, "r") as f:
|
|
244
|
+
config = json.load(f)
|
|
245
|
+
except Exception as e:
|
|
246
|
+
self.logger.error(f"Error reading config: {e}")
|
|
247
|
+
return {}
|
|
248
|
+
|
|
249
|
+
results = {}
|
|
250
|
+
for service_name, service_config in config.get("mcpServers", {}).items():
|
|
251
|
+
command_path = service_config.get("command", "")
|
|
252
|
+
results[service_name] = Path(command_path).exists()
|
|
253
|
+
|
|
254
|
+
return results
|
|
255
|
+
|
|
256
|
+
def install_missing_services(self) -> Tuple[bool, str]:
|
|
257
|
+
"""
|
|
258
|
+
Install missing MCP services via pipx.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Tuple of (success, message)
|
|
262
|
+
"""
|
|
263
|
+
missing = []
|
|
264
|
+
for service_name in self.PIPX_SERVICES:
|
|
265
|
+
if not self.detect_service_path(service_name):
|
|
266
|
+
missing.append(service_name)
|
|
267
|
+
|
|
268
|
+
if not missing:
|
|
269
|
+
return True, "All MCP services are already installed"
|
|
270
|
+
|
|
271
|
+
installed = []
|
|
272
|
+
failed = []
|
|
273
|
+
|
|
274
|
+
for service_name in missing:
|
|
275
|
+
try:
|
|
276
|
+
self.logger.info(f"Installing {service_name} via pipx...")
|
|
277
|
+
result = subprocess.run(
|
|
278
|
+
["pipx", "install", service_name],
|
|
279
|
+
capture_output=True,
|
|
280
|
+
text=True,
|
|
281
|
+
check=True,
|
|
282
|
+
)
|
|
283
|
+
installed.append(service_name)
|
|
284
|
+
self.logger.info(f"Successfully installed {service_name}")
|
|
285
|
+
except subprocess.CalledProcessError as e:
|
|
286
|
+
failed.append(service_name)
|
|
287
|
+
self.logger.error(f"Failed to install {service_name}: {e.stderr}")
|
|
288
|
+
|
|
289
|
+
if failed:
|
|
290
|
+
return False, f"Failed to install: {', '.join(failed)}"
|
|
291
|
+
elif installed:
|
|
292
|
+
return True, f"Successfully installed: {', '.join(installed)}"
|
|
293
|
+
else:
|
|
294
|
+
return True, "No services needed installation"
|
|
@@ -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",
|
|
@@ -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,28 @@ 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 = await self.external_services.initialize_services()
|
|
217
|
+
|
|
218
|
+
if external_services and self.registry:
|
|
219
|
+
for service in external_services:
|
|
220
|
+
try:
|
|
221
|
+
if self.registry.register_tool(service, category="external"):
|
|
222
|
+
self.logger.info(f"Registered external service: {service.service_name}")
|
|
223
|
+
else:
|
|
224
|
+
self.logger.warning(f"Failed to register external service: {service.service_name}")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
self.logger.warning(f"Error registering {service.service_name}: {e}")
|
|
227
|
+
|
|
228
|
+
self.logger.info(f"Initialized {len(external_services)} external MCP services")
|
|
229
|
+
except Exception as e:
|
|
230
|
+
self.logger.warning(f"Failed to initialize external MCP services: {e}")
|
|
231
|
+
self.external_services = None
|
|
232
|
+
|
|
202
233
|
# Initialize communication handler with fallback
|
|
203
234
|
try:
|
|
204
235
|
self.communication = StdioHandler()
|
|
@@ -371,6 +402,13 @@ class MCPGatewayOrchestrator:
|
|
|
371
402
|
except Exception as e:
|
|
372
403
|
self.logger.warning(f"Error during communication shutdown: {e}")
|
|
373
404
|
|
|
405
|
+
# Shutdown external services
|
|
406
|
+
if self.external_services:
|
|
407
|
+
try:
|
|
408
|
+
await self.external_services.shutdown()
|
|
409
|
+
except Exception as e:
|
|
410
|
+
self.logger.warning(f"Error during external services shutdown: {e}")
|
|
411
|
+
|
|
374
412
|
self.logger.info("MCP Gateway shutdown complete")
|
|
375
413
|
|
|
376
414
|
except Exception as e:
|